chore(ui): remove obsolete pre-rewrite views and viewmodels
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,8 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
using ClaudeDo.Data.Git;
|
using ClaudeDo.Data.Git;
|
||||||
using ClaudeDo.Data.Repositories;
|
|
||||||
using ClaudeDo.Ui;
|
using ClaudeDo.Ui;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels;
|
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
using ClaudeDo.Ui.ViewModels.Islands;
|
||||||
using ClaudeDo.Ui.ViewModels.Modals;
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -77,31 +75,7 @@ sealed class Program
|
|||||||
sc.AddSingleton(sp => new WorkerClient(sp.GetRequiredService<AppSettings>().SignalRUrl));
|
sc.AddSingleton(sp => new WorkerClient(sp.GetRequiredService<AppSettings>().SignalRUrl));
|
||||||
|
|
||||||
// ViewModels
|
// ViewModels
|
||||||
sc.AddTransient<ListEditorViewModel>();
|
|
||||||
sc.AddTransient<TaskEditorViewModel>();
|
|
||||||
sc.AddTransient<WorktreeModalViewModel>();
|
sc.AddTransient<WorktreeModalViewModel>();
|
||||||
sc.AddSingleton<StatusBarViewModel>();
|
|
||||||
sc.AddSingleton<TaskDetailViewModel>();
|
|
||||||
sc.AddSingleton<TaskListViewModel>(sp =>
|
|
||||||
{
|
|
||||||
var dbFactory = sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>();
|
|
||||||
var worker = sp.GetRequiredService<WorkerClient>();
|
|
||||||
var statusBar = sp.GetRequiredService<StatusBarViewModel>();
|
|
||||||
return new TaskListViewModel(
|
|
||||||
dbFactory, worker,
|
|
||||||
() => sp.GetRequiredService<TaskEditorViewModel>(),
|
|
||||||
msg => statusBar.ShowMessage(msg));
|
|
||||||
});
|
|
||||||
sc.AddSingleton<MainWindowViewModel>(sp =>
|
|
||||||
{
|
|
||||||
return new MainWindowViewModel(
|
|
||||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
|
||||||
sp.GetRequiredService<WorkerClient>(),
|
|
||||||
sp.GetRequiredService<TaskListViewModel>(),
|
|
||||||
sp.GetRequiredService<TaskDetailViewModel>(),
|
|
||||||
sp.GetRequiredService<StatusBarViewModel>(),
|
|
||||||
() => sp.GetRequiredService<ListEditorViewModel>());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Islands shell VMs
|
// Islands shell VMs
|
||||||
sc.AddSingleton<ListsIslandViewModel>();
|
sc.AddSingleton<ListsIslandViewModel>();
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
using ClaudeDo.Data.Models;
|
|
||||||
using ClaudeDo.Ui.Services;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using AgentInfo = ClaudeDo.Data.Models.AgentInfo;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels;
|
|
||||||
|
|
||||||
public partial class ListEditorViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
[ObservableProperty] private string _name = "";
|
|
||||||
[ObservableProperty] private string? _workingDir;
|
|
||||||
[ObservableProperty] private string _defaultCommitType = "chore";
|
|
||||||
[ObservableProperty] private string _windowTitle = "New List";
|
|
||||||
|
|
||||||
// Config fields
|
|
||||||
[ObservableProperty] private string _model = "Sonnet";
|
|
||||||
[ObservableProperty] private string? _systemPrompt;
|
|
||||||
[ObservableProperty] private AgentInfo? _selectedAgent;
|
|
||||||
|
|
||||||
private string? _editId;
|
|
||||||
private DateTime _createdAt;
|
|
||||||
private TaskCompletionSource<ListEntity?> _tcs = new();
|
|
||||||
|
|
||||||
public event Action? RequestClose;
|
|
||||||
|
|
||||||
public static string[] CommitTypes { get; } =
|
|
||||||
["chore", "feat", "fix", "refactor", "docs", "test", "perf", "style", "build", "ci"];
|
|
||||||
|
|
||||||
public static string[] ModelDisplayNames { get; } = ["Sonnet", "Opus", "Haiku"];
|
|
||||||
|
|
||||||
private static readonly Dictionary<string, string> ModelToId = new()
|
|
||||||
{
|
|
||||||
["Sonnet"] = "claude-sonnet-4-6",
|
|
||||||
["Opus"] = "claude-opus-4-6",
|
|
||||||
["Haiku"] = "claude-haiku-4-5",
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly Dictionary<string, string> IdToModel =
|
|
||||||
ModelToId.ToDictionary(kv => kv.Value, kv => kv.Key);
|
|
||||||
|
|
||||||
public static string ModelIdToDisplay(string? modelId) =>
|
|
||||||
modelId is not null && IdToModel.TryGetValue(modelId, out var display) ? display : "Sonnet";
|
|
||||||
|
|
||||||
public static string? ModelDisplayToId(string display) =>
|
|
||||||
ModelToId.TryGetValue(display, out var id) ? id : null;
|
|
||||||
|
|
||||||
public List<AgentInfo> AvailableAgents { get; set; } = [];
|
|
||||||
|
|
||||||
public async Task LoadAgentsAsync(WorkerClient worker)
|
|
||||||
{
|
|
||||||
AvailableAgents = await worker.GetAgentsAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void InitForCreate()
|
|
||||||
{
|
|
||||||
_tcs = new TaskCompletionSource<ListEntity?>();
|
|
||||||
_editId = null;
|
|
||||||
_createdAt = DateTime.UtcNow;
|
|
||||||
WindowTitle = "New List";
|
|
||||||
}
|
|
||||||
|
|
||||||
public void InitForEdit(ListEntity entity, ListConfigEntity? config)
|
|
||||||
{
|
|
||||||
_tcs = new TaskCompletionSource<ListEntity?>();
|
|
||||||
_editId = entity.Id;
|
|
||||||
_createdAt = entity.CreatedAt;
|
|
||||||
Name = entity.Name;
|
|
||||||
WorkingDir = entity.WorkingDir;
|
|
||||||
DefaultCommitType = entity.DefaultCommitType;
|
|
||||||
WindowTitle = $"Edit List: {entity.Name}";
|
|
||||||
|
|
||||||
if (config is not null)
|
|
||||||
{
|
|
||||||
Model = ModelIdToDisplay(config.Model);
|
|
||||||
SystemPrompt = config.SystemPrompt;
|
|
||||||
SelectedAgent = AvailableAgents.FirstOrDefault(a => a.Path == config.AgentPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ListConfigEntity? BuildConfig(string listId)
|
|
||||||
{
|
|
||||||
var modelId = ModelDisplayToId(Model);
|
|
||||||
if (modelId is null && SystemPrompt is null && SelectedAgent is null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new ListConfigEntity
|
|
||||||
{
|
|
||||||
ListId = listId,
|
|
||||||
Model = modelId,
|
|
||||||
SystemPrompt = string.IsNullOrWhiteSpace(SystemPrompt) ? null : SystemPrompt.Trim(),
|
|
||||||
AgentPath = SelectedAgent?.Path,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void Save()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(Name)) return;
|
|
||||||
var entity = new ListEntity
|
|
||||||
{
|
|
||||||
Id = _editId ?? Guid.NewGuid().ToString(),
|
|
||||||
Name = Name.Trim(),
|
|
||||||
CreatedAt = _createdAt,
|
|
||||||
WorkingDir = string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir.Trim(),
|
|
||||||
DefaultCommitType = DefaultCommitType,
|
|
||||||
};
|
|
||||||
_tcs.TrySetResult(entity);
|
|
||||||
RequestClose?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void Cancel()
|
|
||||||
{
|
|
||||||
_tcs.TrySetResult(null);
|
|
||||||
RequestClose?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnWindowClosed()
|
|
||||||
{
|
|
||||||
_tcs.TrySetResult(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<ListEntity?> ShowAndWaitAsync() => _tcs.Task;
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Avalonia.Media;
|
|
||||||
using ClaudeDo.Data.Models;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels;
|
|
||||||
|
|
||||||
public partial class ListItemViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
[ObservableProperty] private string _name;
|
|
||||||
[ObservableProperty] private string? _workingDir;
|
|
||||||
[ObservableProperty] private string _defaultCommitType;
|
|
||||||
|
|
||||||
private static readonly IBrush[] DotPalette =
|
|
||||||
[
|
|
||||||
new SolidColorBrush(Color.Parse("#3d9474")), // green
|
|
||||||
new SolidColorBrush(Color.Parse("#5571a1")), // blue
|
|
||||||
new SolidColorBrush(Color.Parse("#d4964a")), // amber
|
|
||||||
new SolidColorBrush(Color.Parse("#7c6aad")), // purple
|
|
||||||
new SolidColorBrush(Color.Parse("#c25d6a")), // rose
|
|
||||||
];
|
|
||||||
|
|
||||||
public IBrush DotBrush => DotPalette[Math.Abs(Id.GetHashCode()) % DotPalette.Length];
|
|
||||||
|
|
||||||
public string Id { get; }
|
|
||||||
|
|
||||||
public ListItemViewModel(ListEntity entity)
|
|
||||||
{
|
|
||||||
Id = entity.Id;
|
|
||||||
_name = entity.Name;
|
|
||||||
_workingDir = entity.WorkingDir;
|
|
||||||
_defaultCommitType = entity.DefaultCommitType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ListEntity ToEntity() => new()
|
|
||||||
{
|
|
||||||
Id = Id,
|
|
||||||
Name = Name,
|
|
||||||
CreatedAt = DateTime.MinValue, // not used for update
|
|
||||||
WorkingDir = string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir,
|
|
||||||
DefaultCommitType = DefaultCommitType,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
|
||||||
using ClaudeDo.Data;
|
|
||||||
using ClaudeDo.Data.Models;
|
|
||||||
using ClaudeDo.Data.Repositories;
|
|
||||||
using ClaudeDo.Ui.Services;
|
|
||||||
using ClaudeDo.Ui.Views;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels;
|
|
||||||
|
|
||||||
public partial class MainWindowViewModel : ViewModelBase, IDisposable
|
|
||||||
{
|
|
||||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
|
||||||
private readonly WorkerClient _worker;
|
|
||||||
private readonly Func<ListEditorViewModel> _listEditorFactory;
|
|
||||||
|
|
||||||
public ObservableCollection<ListItemViewModel> Lists { get; } = new();
|
|
||||||
|
|
||||||
[ObservableProperty] private ListItemViewModel? _selectedList;
|
|
||||||
|
|
||||||
public TaskListViewModel TaskList { get; }
|
|
||||||
public TaskDetailViewModel TaskDetail { get; }
|
|
||||||
public StatusBarViewModel StatusBar { get; }
|
|
||||||
|
|
||||||
private readonly Action<string> _onTaskChanged;
|
|
||||||
private readonly Action<string> _onTaskSubtasksChanged;
|
|
||||||
|
|
||||||
public MainWindowViewModel(
|
|
||||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
|
||||||
WorkerClient worker,
|
|
||||||
TaskListViewModel taskList,
|
|
||||||
TaskDetailViewModel taskDetail,
|
|
||||||
StatusBarViewModel statusBar,
|
|
||||||
Func<ListEditorViewModel> listEditorFactory)
|
|
||||||
{
|
|
||||||
_dbFactory = dbFactory;
|
|
||||||
_worker = worker;
|
|
||||||
_listEditorFactory = listEditorFactory;
|
|
||||||
TaskList = taskList;
|
|
||||||
TaskDetail = taskDetail;
|
|
||||||
StatusBar = statusBar;
|
|
||||||
|
|
||||||
_onTaskChanged = taskId => _ = TaskList.RefreshSingleAsync(taskId);
|
|
||||||
_onTaskSubtasksChanged = taskId =>
|
|
||||||
{
|
|
||||||
if (TaskDetail.CurrentTaskId == taskId)
|
|
||||||
_ = TaskDetail.RefreshSubtasksFromDbAsync();
|
|
||||||
};
|
|
||||||
TaskList.SelectedTaskChanged += OnSelectedTaskChanged;
|
|
||||||
TaskList.TaskSubtasksChanged += _onTaskSubtasksChanged;
|
|
||||||
TaskDetail.TaskChanged += _onTaskChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
TaskList.SelectedTaskChanged -= OnSelectedTaskChanged;
|
|
||||||
TaskList.TaskSubtasksChanged -= _onTaskSubtasksChanged;
|
|
||||||
TaskDetail.TaskChanged -= _onTaskChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task InitializeAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var listRepo = new ListRepository(context);
|
|
||||||
var lists = await listRepo.GetAllAsync();
|
|
||||||
foreach (var l in lists)
|
|
||||||
Lists.Add(new ListItemViewModel(l));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
StatusBar.ShowMessage($"Error loading lists: {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = _worker.StartAsync().ContinueWith(t =>
|
|
||||||
{
|
|
||||||
if (t.IsFaulted)
|
|
||||||
System.Diagnostics.Debug.WriteLine($"Worker connection failed: {t.Exception?.Message}");
|
|
||||||
}, TaskScheduler.Default);
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnSelectedListChanged(ListItemViewModel? value)
|
|
||||||
{
|
|
||||||
_ = TaskList.LoadAsync(value?.Id);
|
|
||||||
TaskDetail.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnSelectedTaskChanged(TaskItemViewModel? task)
|
|
||||||
{
|
|
||||||
if (task is null)
|
|
||||||
TaskDetail.Clear();
|
|
||||||
else
|
|
||||||
await TaskDetail.LoadAsync(task.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task AddList()
|
|
||||||
{
|
|
||||||
var editor = _listEditorFactory();
|
|
||||||
await editor.LoadAgentsAsync(_worker);
|
|
||||||
editor.InitForCreate();
|
|
||||||
|
|
||||||
var window = new ListEditorView { DataContext = editor };
|
|
||||||
editor.RequestClose += () => window.Close();
|
|
||||||
window.Closed += (_, _) => editor.OnWindowClosed();
|
|
||||||
_ = ShowDialogAsync(window);
|
|
||||||
|
|
||||||
var entity = await editor.ShowAndWaitAsync();
|
|
||||||
if (entity is null) return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var listRepo = new ListRepository(context);
|
|
||||||
await listRepo.AddAsync(entity);
|
|
||||||
var configEntity = editor.BuildConfig(entity.Id);
|
|
||||||
if (configEntity is not null)
|
|
||||||
await listRepo.SetConfigAsync(configEntity);
|
|
||||||
Lists.Add(new ListItemViewModel(entity));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
StatusBar.ShowMessage($"Error creating list: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task EditList()
|
|
||||||
{
|
|
||||||
if (SelectedList is null) return;
|
|
||||||
|
|
||||||
ListEntity? existing;
|
|
||||||
ListConfigEntity? config;
|
|
||||||
using (var context = _dbFactory.CreateDbContext())
|
|
||||||
{
|
|
||||||
var listRepo = new ListRepository(context);
|
|
||||||
existing = await listRepo.GetByIdAsync(SelectedList.Id);
|
|
||||||
if (existing is null) return;
|
|
||||||
config = await listRepo.GetConfigAsync(existing.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
var editor = _listEditorFactory();
|
|
||||||
await editor.LoadAgentsAsync(_worker);
|
|
||||||
editor.InitForEdit(existing, config);
|
|
||||||
|
|
||||||
var window = new ListEditorView { DataContext = editor };
|
|
||||||
editor.RequestClose += () => window.Close();
|
|
||||||
window.Closed += (_, _) => editor.OnWindowClosed();
|
|
||||||
_ = ShowDialogAsync(window);
|
|
||||||
|
|
||||||
var entity = await editor.ShowAndWaitAsync();
|
|
||||||
if (entity is null) return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var listRepo = new ListRepository(context);
|
|
||||||
await listRepo.UpdateAsync(entity);
|
|
||||||
var configEntity = editor.BuildConfig(entity.Id);
|
|
||||||
if (configEntity is not null)
|
|
||||||
await listRepo.SetConfigAsync(configEntity);
|
|
||||||
SelectedList.Name = entity.Name;
|
|
||||||
SelectedList.WorkingDir = entity.WorkingDir;
|
|
||||||
SelectedList.DefaultCommitType = entity.DefaultCommitType;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
StatusBar.ShowMessage($"Error updating list: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[ObservableProperty] private bool _isDeleteConfirmVisible;
|
|
||||||
private ListItemViewModel? _pendingDeleteList;
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void DeleteList()
|
|
||||||
{
|
|
||||||
if (SelectedList is null) return;
|
|
||||||
_pendingDeleteList = SelectedList;
|
|
||||||
IsDeleteConfirmVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task ConfirmDeleteList()
|
|
||||||
{
|
|
||||||
IsDeleteConfirmVisible = false;
|
|
||||||
if (_pendingDeleteList is null) return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var listRepo = new ListRepository(context);
|
|
||||||
await listRepo.DeleteAsync(_pendingDeleteList.Id);
|
|
||||||
Lists.Remove(_pendingDeleteList);
|
|
||||||
if (SelectedList == _pendingDeleteList)
|
|
||||||
SelectedList = null;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
StatusBar.ShowMessage($"Error deleting list: {ex.Message}");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_pendingDeleteList = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void CancelDeleteList()
|
|
||||||
{
|
|
||||||
IsDeleteConfirmVisible = false;
|
|
||||||
_pendingDeleteList = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task ShowDialogAsync(Window dialog)
|
|
||||||
{
|
|
||||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop
|
|
||||||
&& desktop.MainWindow is not null)
|
|
||||||
{
|
|
||||||
await dialog.ShowDialog(desktop.MainWindow);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
dialog.Show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
using System.Collections.Specialized;
|
|
||||||
using ClaudeDo.Ui.Services;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels;
|
|
||||||
|
|
||||||
public partial class StatusBarViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly WorkerClient _worker;
|
|
||||||
|
|
||||||
[ObservableProperty] private string _connectionStatus = "Offline";
|
|
||||||
[ObservableProperty] private string _activeTasksSummary = "";
|
|
||||||
[ObservableProperty] private string _statusMessage = "";
|
|
||||||
|
|
||||||
public StatusBarViewModel(WorkerClient worker)
|
|
||||||
{
|
|
||||||
_worker = worker;
|
|
||||||
|
|
||||||
worker.PropertyChanged += (_, e) =>
|
|
||||||
{
|
|
||||||
if (e.PropertyName == nameof(WorkerClient.IsConnected) ||
|
|
||||||
e.PropertyName == nameof(WorkerClient.IsReconnecting))
|
|
||||||
{
|
|
||||||
ConnectionStatus = worker.IsConnected ? "Online"
|
|
||||||
: worker.IsReconnecting ? "Connecting..."
|
|
||||||
: "Offline";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
worker.ActiveTasks.CollectionChanged += OnActiveTasksChanged;
|
|
||||||
RefreshActiveSummary();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnActiveTasksChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
|
|
||||||
RefreshActiveSummary();
|
|
||||||
|
|
||||||
private void RefreshActiveSummary()
|
|
||||||
{
|
|
||||||
if (_worker.ActiveTasks.Count == 0)
|
|
||||||
{
|
|
||||||
ActiveTasksSummary = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var parts = _worker.ActiveTasks
|
|
||||||
.Select(t => $"{t.Slot}: {Shorten(t.TaskId)}")
|
|
||||||
.ToList();
|
|
||||||
ActiveTasksSummary = string.Join(" | ", parts);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Shorten(string id) =>
|
|
||||||
id.Length > 8 ? id[..8] : id;
|
|
||||||
|
|
||||||
public void ShowMessage(string msg) => StatusMessage = msg;
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using ClaudeDo.Data.Models;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels;
|
|
||||||
|
|
||||||
public partial class SubtaskItemViewModel : ObservableObject
|
|
||||||
{
|
|
||||||
[ObservableProperty] private string _title = string.Empty;
|
|
||||||
[ObservableProperty] private bool _completed;
|
|
||||||
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
public string? OriginalTitle { get; set; }
|
|
||||||
public bool OriginalCompleted { get; set; }
|
|
||||||
|
|
||||||
public static SubtaskItemViewModel From(SubtaskEntity e) => new()
|
|
||||||
{
|
|
||||||
Id = e.Id,
|
|
||||||
Title = e.Title,
|
|
||||||
Completed = e.Completed,
|
|
||||||
OriginalTitle = e.Title,
|
|
||||||
OriginalCompleted = e.Completed,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,583 +0,0 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using ClaudeDo.Data;
|
|
||||||
using ClaudeDo.Data.Git;
|
|
||||||
using ClaudeDo.Data.Models;
|
|
||||||
using ClaudeDo.Data.Repositories;
|
|
||||||
using ClaudeDo.Ui.Helpers;
|
|
||||||
using ClaudeDo.Ui.Services;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels;
|
|
||||||
|
|
||||||
public partial class TaskDetailViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
|
||||||
private readonly GitService _git;
|
|
||||||
private readonly WorkerClient _worker;
|
|
||||||
|
|
||||||
[ObservableProperty] private string _title = "";
|
|
||||||
[ObservableProperty] private string? _description;
|
|
||||||
[ObservableProperty] private string? _result;
|
|
||||||
[ObservableProperty] private string? _logPath;
|
|
||||||
[ObservableProperty] private string _statusText = "";
|
|
||||||
[ObservableProperty] private string _statusChoice = "Manual";
|
|
||||||
[ObservableProperty] private string _commitType = "chore";
|
|
||||||
[ObservableProperty] private string _modelChoice = "(list default)";
|
|
||||||
[ObservableProperty] private string? _systemPromptOverride;
|
|
||||||
[ObservableProperty] private AgentInfo? _selectedAgent;
|
|
||||||
public List<AgentInfo> AvailableAgents { get; } = [];
|
|
||||||
|
|
||||||
public static string[] StatusChoices { get; } = ["Manual", "Queued", "Running", "Done", "Failed"];
|
|
||||||
public static string[] CommitTypes { get; } = ["feat", "fix", "refactor", "docs", "test", "chore", "ci", "perf", "style", "build"];
|
|
||||||
public static string[] ModelChoices { get; } = ["(list default)", "Sonnet", "Opus", "Haiku"];
|
|
||||||
|
|
||||||
// Worktree
|
|
||||||
[ObservableProperty] private bool _hasWorktree;
|
|
||||||
[ObservableProperty] private string? _branchName;
|
|
||||||
[ObservableProperty] private string? _diffStat;
|
|
||||||
[ObservableProperty] private string? _worktreePath;
|
|
||||||
[ObservableProperty] private string _worktreeState = "";
|
|
||||||
|
|
||||||
// Live stream
|
|
||||||
[ObservableProperty] private string _liveText = "";
|
|
||||||
private StreamLineFormatter _formatter = new();
|
|
||||||
public ObservableCollection<TagEntity> Tags { get; } = new();
|
|
||||||
[ObservableProperty] private string _newTagInput = "";
|
|
||||||
public ObservableCollection<SubtaskItemViewModel> Subtasks { get; } = new();
|
|
||||||
|
|
||||||
private string? _taskId;
|
|
||||||
public string? CurrentTaskId => _taskId;
|
|
||||||
private string? _listId;
|
|
||||||
private bool _isLoading;
|
|
||||||
// Cancels an in-flight LoadAsync when a new TaskUpdated event arrives
|
|
||||||
// before the previous load finished — prevents torn state on _taskId,
|
|
||||||
// Subtasks, Tags, etc.
|
|
||||||
private CancellationTokenSource? _loadCts;
|
|
||||||
|
|
||||||
public event Action<string>? TaskChanged;
|
|
||||||
|
|
||||||
public TaskDetailViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, GitService git, WorkerClient worker)
|
|
||||||
{
|
|
||||||
_dbFactory = dbFactory;
|
|
||||||
_git = git;
|
|
||||||
_worker = worker;
|
|
||||||
|
|
||||||
worker.TaskMessageEvent += OnTaskMessage;
|
|
||||||
worker.WorktreeUpdatedEvent += OnWorktreeUpdated;
|
|
||||||
worker.TaskUpdatedEvent += OnTaskUpdated;
|
|
||||||
worker.RunNowRequestedEvent += OnRunNowRequested;
|
|
||||||
worker.TaskStartedEvent += OnTaskStarted;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LoadAsync(string taskId)
|
|
||||||
{
|
|
||||||
// Cancel any in-flight load so rapid TaskUpdated events don't race
|
|
||||||
// on _taskId / Subtasks / Tags. The newest caller wins.
|
|
||||||
var oldCts = _loadCts;
|
|
||||||
var cts = new CancellationTokenSource();
|
|
||||||
_loadCts = cts;
|
|
||||||
oldCts?.Cancel();
|
|
||||||
oldCts?.Dispose();
|
|
||||||
var ct = cts.Token;
|
|
||||||
|
|
||||||
_taskId = taskId;
|
|
||||||
HasWorktree = false;
|
|
||||||
WorktreeState = "";
|
|
||||||
BranchName = null;
|
|
||||||
DiffStat = null;
|
|
||||||
WorktreePath = null;
|
|
||||||
OnPropertyChanged(nameof(CanWorktreeAction));
|
|
||||||
LiveText = "";
|
|
||||||
_formatter = new StreamLineFormatter();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
TaskEntity? task;
|
|
||||||
List<TagEntity> tags;
|
|
||||||
List<SubtaskEntity> subtasks;
|
|
||||||
|
|
||||||
using (var context = _dbFactory.CreateDbContext())
|
|
||||||
{
|
|
||||||
var taskRepo = new TaskRepository(context);
|
|
||||||
task = await taskRepo.GetByIdAsync(taskId, ct);
|
|
||||||
if (task is null) return;
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
tags = await taskRepo.GetTagsAsync(taskId, ct);
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
var subtaskRepo = new SubtaskRepository(context);
|
|
||||||
subtasks = await subtaskRepo.GetByTaskIdAsync(taskId, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
if (AvailableAgents.Count == 0)
|
|
||||||
{
|
|
||||||
var agents = await _worker.GetAgentsAsync();
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
AvailableAgents.AddRange(agents);
|
|
||||||
OnPropertyChanged(nameof(AvailableAgents));
|
|
||||||
}
|
|
||||||
|
|
||||||
_isLoading = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_listId = task.ListId;
|
|
||||||
Title = task.Title;
|
|
||||||
Description = task.Description;
|
|
||||||
Result = task.Result;
|
|
||||||
LogPath = task.LogPath;
|
|
||||||
if (task.LogPath is not null
|
|
||||||
&& task.Status is Data.Models.TaskStatus.Done or Data.Models.TaskStatus.Failed
|
|
||||||
&& File.Exists(task.LogPath))
|
|
||||||
{
|
|
||||||
_formatter = new StreamLineFormatter();
|
|
||||||
LiveText = await Task.Run(() => _formatter.FormatFile(task.LogPath), ct);
|
|
||||||
}
|
|
||||||
StatusText = task.Status.ToString().ToLowerInvariant();
|
|
||||||
StatusChoice = task.Status.ToString();
|
|
||||||
CommitType = task.CommitType;
|
|
||||||
ModelChoice = task.Model is not null
|
|
||||||
? ListEditorViewModel.ModelIdToDisplay(task.Model)
|
|
||||||
: "(list default)";
|
|
||||||
SystemPromptOverride = task.SystemPrompt;
|
|
||||||
if (task.AgentPath is not null)
|
|
||||||
{
|
|
||||||
var match = AvailableAgents.FirstOrDefault(a => a.Path == task.AgentPath);
|
|
||||||
if (match is null)
|
|
||||||
{
|
|
||||||
match = new AgentInfo(Path.GetFileNameWithoutExtension(task.AgentPath), "(external)", task.AgentPath);
|
|
||||||
AvailableAgents.Add(match);
|
|
||||||
OnPropertyChanged(nameof(AvailableAgents));
|
|
||||||
}
|
|
||||||
SelectedAgent = match;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SelectedAgent = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Tags.Clear();
|
|
||||||
foreach (var tag in tags)
|
|
||||||
Tags.Add(tag);
|
|
||||||
|
|
||||||
// Tear down old subtask subscriptions before replacing them.
|
|
||||||
foreach (var old in Subtasks) old.PropertyChanged -= OnSubtaskPropertyChanged;
|
|
||||||
Subtasks.Clear();
|
|
||||||
foreach (var s in subtasks)
|
|
||||||
{
|
|
||||||
var vm = SubtaskItemViewModel.From(s);
|
|
||||||
vm.PropertyChanged += OnSubtaskPropertyChanged;
|
|
||||||
Subtasks.Add(vm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_isLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await LoadWorktreeAsync(taskId);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// Superseded by a newer LoadAsync — nothing to do.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveAsync()
|
|
||||||
{
|
|
||||||
if (_isLoading || _taskId is null) return;
|
|
||||||
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var taskRepo = new TaskRepository(context);
|
|
||||||
var entity = await taskRepo.GetByIdAsync(_taskId);
|
|
||||||
if (entity is null) return;
|
|
||||||
|
|
||||||
entity.Title = Title;
|
|
||||||
entity.Description = Description;
|
|
||||||
entity.CommitType = CommitType;
|
|
||||||
entity.Model = ModelChoice != "(list default)"
|
|
||||||
? ListEditorViewModel.ModelDisplayToId(ModelChoice)
|
|
||||||
: null;
|
|
||||||
entity.SystemPrompt = string.IsNullOrWhiteSpace(SystemPromptOverride) ? null : SystemPromptOverride.Trim();
|
|
||||||
entity.AgentPath = SelectedAgent?.Path;
|
|
||||||
|
|
||||||
if (Enum.TryParse<Data.Models.TaskStatus>(StatusChoice, true, out var status))
|
|
||||||
entity.Status = status;
|
|
||||||
|
|
||||||
await taskRepo.UpdateAsync(entity);
|
|
||||||
StatusText = entity.Status.ToString().ToLowerInvariant();
|
|
||||||
TaskChanged?.Invoke(_taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task AddTag()
|
|
||||||
{
|
|
||||||
var name = NewTagInput.Trim();
|
|
||||||
if (string.IsNullOrEmpty(name) || _taskId is null) return;
|
|
||||||
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var tagRepo = new TagRepository(context);
|
|
||||||
var taskRepo = new TaskRepository(context);
|
|
||||||
|
|
||||||
var tagId = await tagRepo.GetOrCreateAsync(name);
|
|
||||||
await taskRepo.AddTagAsync(_taskId, tagId);
|
|
||||||
|
|
||||||
Tags.Clear();
|
|
||||||
var tags = await taskRepo.GetTagsAsync(_taskId);
|
|
||||||
foreach (var tag in tags)
|
|
||||||
Tags.Add(tag);
|
|
||||||
|
|
||||||
NewTagInput = "";
|
|
||||||
TaskChanged?.Invoke(_taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task RemoveTag(TagEntity tag)
|
|
||||||
{
|
|
||||||
if (_taskId is null) return;
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var taskRepo = new TaskRepository(context);
|
|
||||||
await taskRepo.RemoveTagAsync(_taskId, tag.Id);
|
|
||||||
Tags.Remove(tag);
|
|
||||||
TaskChanged?.Invoke(_taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task AddSubtask()
|
|
||||||
{
|
|
||||||
if (_taskId is null) return;
|
|
||||||
var entity = new SubtaskEntity
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid().ToString(),
|
|
||||||
TaskId = _taskId,
|
|
||||||
Title = "",
|
|
||||||
Completed = false,
|
|
||||||
OrderNum = Subtasks.Count,
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
};
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var subtaskRepo = new SubtaskRepository(context);
|
|
||||||
await subtaskRepo.AddAsync(entity);
|
|
||||||
var vm = SubtaskItemViewModel.From(entity);
|
|
||||||
vm.PropertyChanged += OnSubtaskPropertyChanged;
|
|
||||||
Subtasks.Add(vm);
|
|
||||||
TaskChanged?.Invoke(_taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task RemoveSubtask(SubtaskItemViewModel item)
|
|
||||||
{
|
|
||||||
if (_taskId is null) return;
|
|
||||||
if (!string.IsNullOrEmpty(item.Id))
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var subtaskRepo = new SubtaskRepository(context);
|
|
||||||
await subtaskRepo.DeleteAsync(item.Id);
|
|
||||||
}
|
|
||||||
item.PropertyChanged -= OnSubtaskPropertyChanged;
|
|
||||||
Subtasks.Remove(item);
|
|
||||||
TaskChanged?.Invoke(_taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnSubtaskPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_isLoading || sender is not SubtaskItemViewModel vm || string.IsNullOrEmpty(vm.Id)) return;
|
|
||||||
if (e.PropertyName is not (nameof(SubtaskItemViewModel.Title) or nameof(SubtaskItemViewModel.Completed))) return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_taskId is null) return;
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var orig = await context.Subtasks.AsNoTracking().FirstOrDefaultAsync(s => s.Id == vm.Id);
|
|
||||||
var subtaskRepo = new SubtaskRepository(context);
|
|
||||||
await subtaskRepo.UpdateAsync(new SubtaskEntity
|
|
||||||
{
|
|
||||||
Id = vm.Id,
|
|
||||||
TaskId = _taskId,
|
|
||||||
Title = vm.Title,
|
|
||||||
Completed = vm.Completed,
|
|
||||||
OrderNum = Subtasks.IndexOf(vm),
|
|
||||||
CreatedAt = orig?.CreatedAt ?? DateTime.UtcNow,
|
|
||||||
});
|
|
||||||
if (e.PropertyName == nameof(SubtaskItemViewModel.Completed))
|
|
||||||
TaskChanged?.Invoke(_taskId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// async void must never throw — surface via Debug.
|
|
||||||
Debug.WriteLine($"[TaskDetailViewModel] Subtask update failed for {vm.Id}: {ex}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RefreshSubtasksFromDbAsync()
|
|
||||||
{
|
|
||||||
if (_taskId is null) return;
|
|
||||||
|
|
||||||
List<SubtaskEntity> subtasks;
|
|
||||||
using (var context = _dbFactory.CreateDbContext())
|
|
||||||
{
|
|
||||||
var subtaskRepo = new SubtaskRepository(context);
|
|
||||||
subtasks = await subtaskRepo.GetByTaskIdAsync(_taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
_isLoading = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var old in Subtasks) old.PropertyChanged -= OnSubtaskPropertyChanged;
|
|
||||||
Subtasks.Clear();
|
|
||||||
foreach (var s in subtasks)
|
|
||||||
{
|
|
||||||
var vm = SubtaskItemViewModel.From(s);
|
|
||||||
vm.PropertyChanged += OnSubtaskPropertyChanged;
|
|
||||||
Subtasks.Add(vm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetAgentFromPath(string path)
|
|
||||||
{
|
|
||||||
var existing = AvailableAgents.FirstOrDefault(a => a.Path == path);
|
|
||||||
if (existing is null)
|
|
||||||
{
|
|
||||||
existing = new AgentInfo(Path.GetFileNameWithoutExtension(path), "(external)", path);
|
|
||||||
AvailableAgents.Add(existing);
|
|
||||||
OnPropertyChanged(nameof(AvailableAgents));
|
|
||||||
}
|
|
||||||
SelectedAgent = existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
// Cancel any load in flight so it doesn't resurrect state after Clear.
|
|
||||||
_loadCts?.Cancel();
|
|
||||||
_loadCts?.Dispose();
|
|
||||||
_loadCts = null;
|
|
||||||
|
|
||||||
_taskId = null;
|
|
||||||
_listId = null;
|
|
||||||
Title = "";
|
|
||||||
Description = null;
|
|
||||||
Result = null;
|
|
||||||
LogPath = null;
|
|
||||||
StatusText = "";
|
|
||||||
HasWorktree = false;
|
|
||||||
LiveText = "";
|
|
||||||
_formatter = new StreamLineFormatter();
|
|
||||||
Tags.Clear();
|
|
||||||
NewTagInput = "";
|
|
||||||
foreach (var s in Subtasks) s.PropertyChanged -= OnSubtaskPropertyChanged;
|
|
||||||
Subtasks.Clear();
|
|
||||||
StatusChoice = "Manual";
|
|
||||||
CommitType = "chore";
|
|
||||||
ModelChoice = "(list default)";
|
|
||||||
SystemPromptOverride = null;
|
|
||||||
SelectedAgent = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadWorktreeAsync(string taskId)
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var wtRepo = new WorktreeRepository(context);
|
|
||||||
var wt = await wtRepo.GetByTaskIdAsync(taskId);
|
|
||||||
HasWorktree = wt is not null;
|
|
||||||
if (wt is not null)
|
|
||||||
{
|
|
||||||
BranchName = wt.BranchName;
|
|
||||||
DiffStat = wt.DiffStat;
|
|
||||||
WorktreePath = wt.Path;
|
|
||||||
WorktreeState = wt.State.ToString().ToLowerInvariant();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
BranchName = null;
|
|
||||||
DiffStat = null;
|
|
||||||
WorktreePath = null;
|
|
||||||
WorktreeState = "";
|
|
||||||
}
|
|
||||||
OnPropertyChanged(nameof(CanWorktreeAction));
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanWorktreeAction => HasWorktree && WorktreeState == "active";
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void OpenWorktree()
|
|
||||||
{
|
|
||||||
if (WorktreePath is null) return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Process.Start(new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = WorktreePath,
|
|
||||||
UseShellExecute = true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.WriteLine($"Failed to open worktree: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void ShowDiff()
|
|
||||||
{
|
|
||||||
if (WorktreePath is null) return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Process.Start(new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = "cmd.exe",
|
|
||||||
Arguments = $"/k git -C \"{WorktreePath}\" diff HEAD~1",
|
|
||||||
UseShellExecute = true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.WriteLine($"Failed to show diff: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task MergeIntoMainAsync()
|
|
||||||
{
|
|
||||||
if (_taskId is null || _listId is null) return;
|
|
||||||
|
|
||||||
WorktreeEntity? wt;
|
|
||||||
ListEntity? list;
|
|
||||||
using (var context = _dbFactory.CreateDbContext())
|
|
||||||
{
|
|
||||||
var wtRepo = new WorktreeRepository(context);
|
|
||||||
wt = await wtRepo.GetByTaskIdAsync(_taskId);
|
|
||||||
var listRepo = new ListRepository(context);
|
|
||||||
list = await listRepo.GetByIdAsync(_listId);
|
|
||||||
}
|
|
||||||
if (wt is null || list?.WorkingDir is null) return;
|
|
||||||
|
|
||||||
await _git.MergeFfOnlyAsync(list.WorkingDir, wt.BranchName);
|
|
||||||
await _git.WorktreeRemoveAsync(list.WorkingDir, wt.Path, force: true);
|
|
||||||
await _git.BranchDeleteAsync(list.WorkingDir, wt.BranchName, force: true);
|
|
||||||
|
|
||||||
using (var context = _dbFactory.CreateDbContext())
|
|
||||||
{
|
|
||||||
var wtRepo = new WorktreeRepository(context);
|
|
||||||
await wtRepo.SetStateAsync(_taskId, Data.Models.WorktreeState.Merged);
|
|
||||||
}
|
|
||||||
await LoadWorktreeAsync(_taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task KeepAsBranchAsync()
|
|
||||||
{
|
|
||||||
if (_taskId is null || _listId is null) return;
|
|
||||||
|
|
||||||
WorktreeEntity? wt;
|
|
||||||
ListEntity? list;
|
|
||||||
using (var context = _dbFactory.CreateDbContext())
|
|
||||||
{
|
|
||||||
var wtRepo = new WorktreeRepository(context);
|
|
||||||
wt = await wtRepo.GetByTaskIdAsync(_taskId);
|
|
||||||
var listRepo = new ListRepository(context);
|
|
||||||
list = await listRepo.GetByIdAsync(_listId);
|
|
||||||
}
|
|
||||||
if (wt is null || list?.WorkingDir is null) return;
|
|
||||||
|
|
||||||
await _git.WorktreeRemoveAsync(list.WorkingDir, wt.Path, force: true);
|
|
||||||
|
|
||||||
using (var context = _dbFactory.CreateDbContext())
|
|
||||||
{
|
|
||||||
var wtRepo = new WorktreeRepository(context);
|
|
||||||
await wtRepo.SetStateAsync(_taskId, Data.Models.WorktreeState.Kept);
|
|
||||||
}
|
|
||||||
await LoadWorktreeAsync(_taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task DiscardAsync()
|
|
||||||
{
|
|
||||||
if (_taskId is null || _listId is null) return;
|
|
||||||
|
|
||||||
WorktreeEntity? wt;
|
|
||||||
ListEntity? list;
|
|
||||||
using (var context = _dbFactory.CreateDbContext())
|
|
||||||
{
|
|
||||||
var wtRepo = new WorktreeRepository(context);
|
|
||||||
wt = await wtRepo.GetByTaskIdAsync(_taskId);
|
|
||||||
var listRepo = new ListRepository(context);
|
|
||||||
list = await listRepo.GetByIdAsync(_listId);
|
|
||||||
}
|
|
||||||
if (wt is null || list?.WorkingDir is null) return;
|
|
||||||
|
|
||||||
await _git.WorktreeRemoveAsync(list.WorkingDir, wt.Path, force: true);
|
|
||||||
await _git.BranchDeleteAsync(list.WorkingDir, wt.BranchName, force: true);
|
|
||||||
|
|
||||||
using (var context = _dbFactory.CreateDbContext())
|
|
||||||
{
|
|
||||||
var wtRepo = new WorktreeRepository(context);
|
|
||||||
await wtRepo.SetStateAsync(_taskId, Data.Models.WorktreeState.Discarded);
|
|
||||||
}
|
|
||||||
await LoadWorktreeAsync(_taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTaskMessage(string taskId, string line)
|
|
||||||
{
|
|
||||||
if (taskId != _taskId) return;
|
|
||||||
var formatted = _formatter.FormatLine(line);
|
|
||||||
if (formatted is not null)
|
|
||||||
{
|
|
||||||
LiveText += formatted;
|
|
||||||
if (LiveText.Length > 50_000)
|
|
||||||
LiveText = StreamLineFormatter.Trim(LiveText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRunNowRequested(string taskId)
|
|
||||||
{
|
|
||||||
if (taskId != _taskId) return;
|
|
||||||
StatusText = "starting...";
|
|
||||||
LiveText = "";
|
|
||||||
_formatter = new StreamLineFormatter();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTaskStarted(string slot, string taskId, DateTime startedAt)
|
|
||||||
{
|
|
||||||
if (taskId != _taskId) return;
|
|
||||||
StatusText = "running";
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnWorktreeUpdated(string taskId)
|
|
||||||
{
|
|
||||||
if (taskId != _taskId) return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await LoadWorktreeAsync(taskId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// async void must never throw.
|
|
||||||
Debug.WriteLine($"[TaskDetailViewModel] OnWorktreeUpdated failed for {taskId}: {ex}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnTaskUpdated(string taskId)
|
|
||||||
{
|
|
||||||
if (taskId != _taskId) return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await LoadAsync(taskId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// async void must never throw.
|
|
||||||
Debug.WriteLine($"[TaskDetailViewModel] OnTaskUpdated failed for {taskId}: {ex}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.IO;
|
|
||||||
using ClaudeDo.Data;
|
|
||||||
using ClaudeDo.Data.Models;
|
|
||||||
using ClaudeDo.Data.Repositories;
|
|
||||||
using ClaudeDo.Ui.Services;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels;
|
|
||||||
|
|
||||||
public partial class TaskEditorViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
|
||||||
|
|
||||||
[ObservableProperty] private string _title = "";
|
|
||||||
[ObservableProperty] private string? _description;
|
|
||||||
[ObservableProperty] private string _commitType = "chore";
|
|
||||||
[ObservableProperty] private string _statusChoice = "manual";
|
|
||||||
[ObservableProperty] private string _tagsInput = "";
|
|
||||||
[ObservableProperty] private string _windowTitle = "New Task";
|
|
||||||
[ObservableProperty] private string _modelChoice = "(list default)";
|
|
||||||
[ObservableProperty] private string? _systemPromptOverride;
|
|
||||||
[ObservableProperty] private AgentInfo? _selectedAgent;
|
|
||||||
public List<AgentInfo> AvailableAgents { get; set; } = [];
|
|
||||||
public ObservableCollection<SubtaskItemViewModel> Subtasks { get; } = new();
|
|
||||||
|
|
||||||
private string? _editId;
|
|
||||||
private string _listId = "";
|
|
||||||
private DateTime _createdAt;
|
|
||||||
private TaskCompletionSource<TaskEntity?> _tcs = new();
|
|
||||||
|
|
||||||
public event Action? RequestClose;
|
|
||||||
|
|
||||||
public static string[] ModelChoices { get; } = ["(list default)", "Sonnet", "Opus", "Haiku"];
|
|
||||||
|
|
||||||
public static string[] CommitTypes { get; } =
|
|
||||||
["chore", "feat", "fix", "refactor", "docs", "test", "perf", "style", "build", "ci"];
|
|
||||||
|
|
||||||
public static string[] StatusChoices { get; } =
|
|
||||||
["manual", "queued"];
|
|
||||||
|
|
||||||
public TaskEditorViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory)
|
|
||||||
{
|
|
||||||
_dbFactory = dbFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LoadAgentsAsync(WorkerClient worker)
|
|
||||||
{
|
|
||||||
AvailableAgents = await worker.GetAgentsAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetAgentFromPath(string path)
|
|
||||||
{
|
|
||||||
var existing = AvailableAgents.FirstOrDefault(a => a.Path == path);
|
|
||||||
if (existing is null)
|
|
||||||
{
|
|
||||||
existing = new AgentInfo(Path.GetFileNameWithoutExtension(path), "(external)", path);
|
|
||||||
AvailableAgents.Add(existing);
|
|
||||||
OnPropertyChanged(nameof(AvailableAgents));
|
|
||||||
}
|
|
||||||
SelectedAgent = existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyList<string> SelectedTagNames =>
|
|
||||||
TagsInput.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
||||||
.Distinct()
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
public void InitForCreate(string listId, string defaultCommitType = "chore")
|
|
||||||
{
|
|
||||||
_tcs = new TaskCompletionSource<TaskEntity?>();
|
|
||||||
_editId = null;
|
|
||||||
_listId = listId;
|
|
||||||
_createdAt = DateTime.UtcNow;
|
|
||||||
CommitType = defaultCommitType;
|
|
||||||
WindowTitle = "New Task";
|
|
||||||
Subtasks.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task InitForEditAsync(TaskEntity entity, IReadOnlyList<TagEntity> taskTags, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
_tcs = new TaskCompletionSource<TaskEntity?>();
|
|
||||||
_editId = entity.Id;
|
|
||||||
_listId = entity.ListId;
|
|
||||||
_createdAt = entity.CreatedAt;
|
|
||||||
Title = entity.Title;
|
|
||||||
Description = entity.Description;
|
|
||||||
CommitType = entity.CommitType;
|
|
||||||
StatusChoice = entity.Status switch
|
|
||||||
{
|
|
||||||
TaskStatus.Manual => "manual",
|
|
||||||
TaskStatus.Queued => "queued",
|
|
||||||
_ => entity.Status.ToString().ToLowerInvariant(),
|
|
||||||
};
|
|
||||||
TagsInput = string.Join(", ", taskTags.Select(t => t.Name));
|
|
||||||
ModelChoice = entity.Model is not null
|
|
||||||
? ListEditorViewModel.ModelIdToDisplay(entity.Model)
|
|
||||||
: "(list default)";
|
|
||||||
SystemPromptOverride = entity.SystemPrompt;
|
|
||||||
|
|
||||||
if (entity.AgentPath is not null)
|
|
||||||
{
|
|
||||||
var match = AvailableAgents.FirstOrDefault(a => a.Path == entity.AgentPath);
|
|
||||||
if (match is null)
|
|
||||||
{
|
|
||||||
match = new AgentInfo(Path.GetFileNameWithoutExtension(entity.AgentPath), "(external)", entity.AgentPath);
|
|
||||||
AvailableAgents.Add(match);
|
|
||||||
OnPropertyChanged(nameof(AvailableAgents));
|
|
||||||
}
|
|
||||||
SelectedAgent = match;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SelectedAgent = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
WindowTitle = $"Edit Task: {entity.Title}";
|
|
||||||
|
|
||||||
Subtasks.Clear();
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var subtaskRepo = new SubtaskRepository(context);
|
|
||||||
var list = await subtaskRepo.GetByTaskIdAsync(entity.Id, ct);
|
|
||||||
foreach (var s in list)
|
|
||||||
Subtasks.Add(SubtaskItemViewModel.From(s));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep old sync overload for callers that haven't loaded agents yet
|
|
||||||
public void InitForEdit(TaskEntity entity, IReadOnlyList<TagEntity> taskTags)
|
|
||||||
{
|
|
||||||
_tcs = new TaskCompletionSource<TaskEntity?>();
|
|
||||||
_editId = entity.Id;
|
|
||||||
_listId = entity.ListId;
|
|
||||||
_createdAt = entity.CreatedAt;
|
|
||||||
Title = entity.Title;
|
|
||||||
Description = entity.Description;
|
|
||||||
CommitType = entity.CommitType;
|
|
||||||
StatusChoice = entity.Status switch
|
|
||||||
{
|
|
||||||
TaskStatus.Manual => "manual",
|
|
||||||
TaskStatus.Queued => "queued",
|
|
||||||
_ => entity.Status.ToString().ToLowerInvariant(),
|
|
||||||
};
|
|
||||||
TagsInput = string.Join(", ", taskTags.Select(t => t.Name));
|
|
||||||
ModelChoice = entity.Model is not null
|
|
||||||
? ListEditorViewModel.ModelIdToDisplay(entity.Model)
|
|
||||||
: "(list default)";
|
|
||||||
SystemPromptOverride = entity.SystemPrompt;
|
|
||||||
|
|
||||||
if (entity.AgentPath is not null)
|
|
||||||
{
|
|
||||||
var match = AvailableAgents.FirstOrDefault(a => a.Path == entity.AgentPath);
|
|
||||||
if (match is null)
|
|
||||||
{
|
|
||||||
match = new AgentInfo(Path.GetFileNameWithoutExtension(entity.AgentPath), "(external)", entity.AgentPath);
|
|
||||||
AvailableAgents.Add(match);
|
|
||||||
OnPropertyChanged(nameof(AvailableAgents));
|
|
||||||
}
|
|
||||||
SelectedAgent = match;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SelectedAgent = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
WindowTitle = $"Edit Task: {entity.Title}";
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void AddSubtask() => Subtasks.Add(new SubtaskItemViewModel());
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void RemoveSubtask(SubtaskItemViewModel item) => Subtasks.Remove(item);
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task Save()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(Title)) return;
|
|
||||||
var status = StatusChoice switch
|
|
||||||
{
|
|
||||||
"queued" => TaskStatus.Queued,
|
|
||||||
_ => TaskStatus.Manual,
|
|
||||||
};
|
|
||||||
var taskId = _editId ?? Guid.NewGuid().ToString();
|
|
||||||
var entity = new TaskEntity
|
|
||||||
{
|
|
||||||
Id = taskId,
|
|
||||||
ListId = _listId,
|
|
||||||
Title = Title.Trim(),
|
|
||||||
Description = string.IsNullOrWhiteSpace(Description) ? null : Description.Trim(),
|
|
||||||
Status = status,
|
|
||||||
CommitType = CommitType,
|
|
||||||
CreatedAt = _createdAt,
|
|
||||||
};
|
|
||||||
entity.Model = ModelChoice != "(list default)"
|
|
||||||
? ListEditorViewModel.ModelDisplayToId(ModelChoice)
|
|
||||||
: null;
|
|
||||||
entity.SystemPrompt = string.IsNullOrWhiteSpace(SystemPromptOverride) ? null : SystemPromptOverride.Trim();
|
|
||||||
entity.AgentPath = SelectedAgent?.Path;
|
|
||||||
|
|
||||||
// Persist subtask changes
|
|
||||||
if (_editId is not null)
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var subtaskRepo = new SubtaskRepository(context);
|
|
||||||
var existing = await subtaskRepo.GetByTaskIdAsync(taskId);
|
|
||||||
var existingIds = existing.Select(s => s.Id).ToHashSet();
|
|
||||||
var currentIds = Subtasks.Where(s => s.Id != "").Select(s => s.Id).ToHashSet();
|
|
||||||
|
|
||||||
// Deleted
|
|
||||||
foreach (var id in existingIds.Except(currentIds))
|
|
||||||
await subtaskRepo.DeleteAsync(id);
|
|
||||||
|
|
||||||
// Updated
|
|
||||||
foreach (var (vm, idx) in Subtasks.Select((v, i) => (v, i)))
|
|
||||||
{
|
|
||||||
if (vm.Id == "") continue;
|
|
||||||
if (vm.Title != vm.OriginalTitle || vm.Completed != vm.OriginalCompleted)
|
|
||||||
{
|
|
||||||
var origSub = existing.FirstOrDefault(e => e.Id == vm.Id);
|
|
||||||
await subtaskRepo.UpdateAsync(new SubtaskEntity { Id = vm.Id, TaskId = taskId, Title = vm.Title, Completed = vm.Completed, OrderNum = idx, CreatedAt = origSub?.CreatedAt ?? DateTime.UtcNow });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// update order_num if position changed
|
|
||||||
var orig = existing.FirstOrDefault(e => e.Id == vm.Id);
|
|
||||||
if (orig is not null && orig.OrderNum != idx)
|
|
||||||
await subtaskRepo.UpdateAsync(new SubtaskEntity { Id = vm.Id, TaskId = taskId, Title = vm.Title, Completed = vm.Completed, OrderNum = idx, CreatedAt = orig.CreatedAt });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Added (id == "" means new)
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var subtaskRepo = new SubtaskRepository(context);
|
|
||||||
foreach (var (vm, idx) in Subtasks.Select((v, i) => (v, i)).Where(x => x.v.Id == ""))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(vm.Title)) continue;
|
|
||||||
var newId = Guid.NewGuid().ToString();
|
|
||||||
await subtaskRepo.AddAsync(new SubtaskEntity { Id = newId, TaskId = taskId, Title = vm.Title.Trim(), Completed = vm.Completed, OrderNum = idx, CreatedAt = DateTime.UtcNow });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_tcs.TrySetResult(entity);
|
|
||||||
RequestClose?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void Cancel()
|
|
||||||
{
|
|
||||||
_tcs.TrySetResult(null);
|
|
||||||
RequestClose?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnWindowClosed()
|
|
||||||
{
|
|
||||||
_tcs.TrySetResult(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<TaskEntity?> ShowAndWaitAsync() => _tcs.Task;
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
using Avalonia.Media;
|
|
||||||
using ClaudeDo.Data;
|
|
||||||
using ClaudeDo.Data.Models;
|
|
||||||
using ClaudeDo.Data.Repositories;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels;
|
|
||||||
|
|
||||||
public partial class TaskItemViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
[ObservableProperty] private string _title;
|
|
||||||
[ObservableProperty] private string _statusText;
|
|
||||||
[ObservableProperty] private string _tagsText;
|
|
||||||
[ObservableProperty] private string _commitType;
|
|
||||||
[ObservableProperty] private string? _description;
|
|
||||||
[ObservableProperty] private TaskStatus _status;
|
|
||||||
[ObservableProperty] private bool _isStarting;
|
|
||||||
[ObservableProperty] private bool _isExpanded;
|
|
||||||
[ObservableProperty] private bool _hasSubtasks;
|
|
||||||
[ObservableProperty] private int _subtaskCount;
|
|
||||||
|
|
||||||
public ObservableCollection<SubtaskItemViewModel> Subtasks { get; } = new();
|
|
||||||
|
|
||||||
public string Id { get; }
|
|
||||||
public string ListId { get; }
|
|
||||||
public TaskEntity Entity { get; private set; }
|
|
||||||
|
|
||||||
private readonly Func<string, Task>? _runNow;
|
|
||||||
private readonly Func<bool> _canRunNow;
|
|
||||||
private readonly Func<string, Task>? _toggleDone;
|
|
||||||
private readonly Action<string>? _onSubtasksChanged;
|
|
||||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
|
||||||
private bool _subtasksLoaded;
|
|
||||||
|
|
||||||
public TaskItemViewModel(TaskEntity entity, IReadOnlyList<TagEntity> tags,
|
|
||||||
Func<string, Task>? runNow, Func<bool> canRunNow,
|
|
||||||
IDbContextFactory<ClaudeDoDbContext> dbFactory, int subtaskCount,
|
|
||||||
Func<string, Task>? toggleDone = null,
|
|
||||||
Action<string>? onSubtasksChanged = null)
|
|
||||||
{
|
|
||||||
Entity = entity;
|
|
||||||
Id = entity.Id;
|
|
||||||
ListId = entity.ListId;
|
|
||||||
_title = entity.Title;
|
|
||||||
_status = entity.Status;
|
|
||||||
_statusText = entity.Status.ToString().ToLowerInvariant();
|
|
||||||
_tagsText = string.Join(", ", tags.Select(t => t.Name));
|
|
||||||
_commitType = entity.CommitType;
|
|
||||||
_description = entity.Description;
|
|
||||||
_runNow = runNow;
|
|
||||||
_canRunNow = canRunNow;
|
|
||||||
_toggleDone = toggleDone;
|
|
||||||
_onSubtasksChanged = onSubtasksChanged;
|
|
||||||
_dbFactory = dbFactory;
|
|
||||||
_subtaskCount = subtaskCount;
|
|
||||||
_hasSubtasks = subtaskCount > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsDone => Status == TaskStatus.Done;
|
|
||||||
public bool IsRunning => Status == TaskStatus.Running;
|
|
||||||
public bool CanToggleDone => Status != TaskStatus.Running && Status != TaskStatus.Failed;
|
|
||||||
|
|
||||||
public TextDecorationCollection? TitleDecorations => IsDone
|
|
||||||
? TextDecorations.Strikethrough
|
|
||||||
: null;
|
|
||||||
|
|
||||||
public IBrush TitleForeground => IsDone
|
|
||||||
? new SolidColorBrush(Color.Parse("#5a6578"))
|
|
||||||
: new SolidColorBrush(Color.Parse("#e2e8f0"));
|
|
||||||
|
|
||||||
public double RowOpacity => IsDone ? 0.6 : 1.0;
|
|
||||||
|
|
||||||
public void Refresh(TaskEntity entity, IReadOnlyList<TagEntity> tags)
|
|
||||||
{
|
|
||||||
Entity = entity;
|
|
||||||
Title = entity.Title;
|
|
||||||
Status = entity.Status;
|
|
||||||
StatusText = entity.Status.ToString().ToLowerInvariant();
|
|
||||||
TagsText = string.Join(", ", tags.Select(t => t.Name));
|
|
||||||
CommitType = entity.CommitType;
|
|
||||||
Description = entity.Description;
|
|
||||||
RunNowCommand.NotifyCanExecuteChanged();
|
|
||||||
OnPropertyChanged(nameof(IsDone));
|
|
||||||
OnPropertyChanged(nameof(IsRunning));
|
|
||||||
IsStarting = false;
|
|
||||||
OnPropertyChanged(nameof(CanToggleDone));
|
|
||||||
OnPropertyChanged(nameof(TitleDecorations));
|
|
||||||
OnPropertyChanged(nameof(TitleForeground));
|
|
||||||
OnPropertyChanged(nameof(RowOpacity));
|
|
||||||
ToggleDoneCommand.NotifyCanExecuteChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetStarting()
|
|
||||||
{
|
|
||||||
IsStarting = true;
|
|
||||||
StatusText = "starting...";
|
|
||||||
RunNowCommand.NotifyCanExecuteChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClearStarting()
|
|
||||||
{
|
|
||||||
IsStarting = false;
|
|
||||||
RunNowCommand.NotifyCanExecuteChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanRunNow))]
|
|
||||||
private async Task RunNowAsync()
|
|
||||||
{
|
|
||||||
if (_runNow is not null)
|
|
||||||
await _runNow(Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CanRunNow() =>
|
|
||||||
_canRunNow() && Status != TaskStatus.Running && !IsStarting;
|
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanToggleDone))]
|
|
||||||
private async Task ToggleDone()
|
|
||||||
{
|
|
||||||
if (_toggleDone is not null)
|
|
||||||
await _toggleDone(Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task ToggleExpanded()
|
|
||||||
{
|
|
||||||
IsExpanded = !IsExpanded;
|
|
||||||
if (IsExpanded && !_subtasksLoaded)
|
|
||||||
await LoadSubtasksAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadSubtasksAsync()
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var repo = new SubtaskRepository(context);
|
|
||||||
var entities = await repo.GetByTaskIdAsync(Id);
|
|
||||||
Subtasks.Clear();
|
|
||||||
foreach (var e in entities)
|
|
||||||
Subtasks.Add(SubtaskItemViewModel.From(e));
|
|
||||||
_subtasksLoaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task ToggleSubtaskDone(string subtaskId)
|
|
||||||
{
|
|
||||||
var vm = Subtasks.FirstOrDefault(s => s.Id == subtaskId);
|
|
||||||
if (vm is null) return;
|
|
||||||
vm.Completed = !vm.Completed;
|
|
||||||
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var entity = await context.Subtasks.FindAsync(subtaskId);
|
|
||||||
if (entity is not null)
|
|
||||||
{
|
|
||||||
entity.Completed = vm.Completed;
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
_onSubtasksChanged?.Invoke(Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RefreshSubtasksAsync(int newCount)
|
|
||||||
{
|
|
||||||
SubtaskCount = newCount;
|
|
||||||
HasSubtasks = newCount > 0;
|
|
||||||
if (!HasSubtasks)
|
|
||||||
{
|
|
||||||
IsExpanded = false;
|
|
||||||
Subtasks.Clear();
|
|
||||||
_subtasksLoaded = false;
|
|
||||||
}
|
|
||||||
else if (_subtasksLoaded || IsExpanded)
|
|
||||||
{
|
|
||||||
await LoadSubtasksAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,360 +0,0 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
|
||||||
using ClaudeDo.Data;
|
|
||||||
using ClaudeDo.Data.Models;
|
|
||||||
using ClaudeDo.Data.Repositories;
|
|
||||||
using ClaudeDo.Ui.Services;
|
|
||||||
using ClaudeDo.Ui.Views;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels;
|
|
||||||
|
|
||||||
public partial class TaskListViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
|
||||||
private readonly WorkerClient _worker;
|
|
||||||
private readonly Func<TaskEditorViewModel> _editorFactory;
|
|
||||||
private readonly Action<string> _showMessage;
|
|
||||||
|
|
||||||
public ObservableCollection<TaskItemViewModel> Tasks { get; } = new();
|
|
||||||
|
|
||||||
[ObservableProperty] private TaskItemViewModel? _selectedTask;
|
|
||||||
[ObservableProperty, NotifyCanExecuteChangedFor(nameof(AddTaskCommand))] private string? _currentListId;
|
|
||||||
[ObservableProperty] private string _listName = "Tasks";
|
|
||||||
[ObservableProperty] private string _inlineAddTitle = "";
|
|
||||||
|
|
||||||
public event Action<TaskItemViewModel?>? SelectedTaskChanged;
|
|
||||||
public event Action<string>? TaskSubtasksChanged;
|
|
||||||
|
|
||||||
partial void OnSelectedTaskChanged(TaskItemViewModel? value) =>
|
|
||||||
SelectedTaskChanged?.Invoke(value);
|
|
||||||
|
|
||||||
private void NotifySubtasksChanged(string taskId) =>
|
|
||||||
TaskSubtasksChanged?.Invoke(taskId);
|
|
||||||
|
|
||||||
public TaskListViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, WorkerClient worker,
|
|
||||||
Func<TaskEditorViewModel> editorFactory, Action<string> showMessage)
|
|
||||||
{
|
|
||||||
_dbFactory = dbFactory;
|
|
||||||
_worker = worker;
|
|
||||||
_editorFactory = editorFactory;
|
|
||||||
_showMessage = showMessage;
|
|
||||||
|
|
||||||
worker.TaskUpdatedEvent += OnTaskUpdated;
|
|
||||||
worker.TaskFinishedEvent += (_, taskId, _, _) => OnTaskUpdated(taskId);
|
|
||||||
worker.PropertyChanged += (_, e) =>
|
|
||||||
{
|
|
||||||
if (e.PropertyName == nameof(WorkerClient.IsConnected))
|
|
||||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
foreach (var t in Tasks)
|
|
||||||
t.RunNowCommand.NotifyCanExecuteChanged();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
worker.RunNowRequestedEvent += taskId =>
|
|
||||||
{
|
|
||||||
var item = Tasks.FirstOrDefault(t => t.Id == taskId);
|
|
||||||
item?.SetStarting();
|
|
||||||
};
|
|
||||||
|
|
||||||
worker.TaskStartedEvent += (_, taskId, _) =>
|
|
||||||
{
|
|
||||||
var item = Tasks.FirstOrDefault(t => t.Id == taskId);
|
|
||||||
item?.ClearStarting();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LoadAsync(string? listId)
|
|
||||||
{
|
|
||||||
CurrentListId = listId;
|
|
||||||
Tasks.Clear();
|
|
||||||
SelectedTask = null;
|
|
||||||
|
|
||||||
if (listId is not null)
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var listRepo = new ListRepository(context);
|
|
||||||
var list = await listRepo.GetByIdAsync(listId);
|
|
||||||
ListName = list?.Name ?? "Tasks";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ListName = "Tasks";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (listId is null) return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var taskRepo = new TaskRepository(context);
|
|
||||||
var entities = await taskRepo.GetByListIdAsync(listId);
|
|
||||||
var taskIds = entities.Select(e => e.Id).ToList();
|
|
||||||
var subtaskCounts = await context.Subtasks
|
|
||||||
.Where(s => taskIds.Contains(s.TaskId))
|
|
||||||
.GroupBy(s => s.TaskId)
|
|
||||||
.ToDictionaryAsync(g => g.Key, g => g.Count());
|
|
||||||
foreach (var e in entities)
|
|
||||||
{
|
|
||||||
var tags = await taskRepo.GetEffectiveTagsAsync(e.Id);
|
|
||||||
subtaskCounts.TryGetValue(e.Id, out var count);
|
|
||||||
Tasks.Add(new TaskItemViewModel(e, tags, RunNowAsync, () => _worker.IsConnected,
|
|
||||||
_dbFactory, count, ToggleDoneAsync, NotifySubtasksChanged));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_showMessage($"Error loading tasks: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CanAddTask() => CurrentListId is not null;
|
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanAddTask))]
|
|
||||||
private async Task InlineAdd()
|
|
||||||
{
|
|
||||||
var title = InlineAddTitle.Trim();
|
|
||||||
if (string.IsNullOrEmpty(title) || CurrentListId is null) return;
|
|
||||||
|
|
||||||
string defaultCommitType;
|
|
||||||
using (var context = _dbFactory.CreateDbContext())
|
|
||||||
{
|
|
||||||
var listRepo = new ListRepository(context);
|
|
||||||
var list = await listRepo.GetByIdAsync(CurrentListId);
|
|
||||||
defaultCommitType = list?.DefaultCommitType ?? "chore";
|
|
||||||
}
|
|
||||||
|
|
||||||
var entity = new TaskEntity
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid().ToString(),
|
|
||||||
ListId = CurrentListId,
|
|
||||||
Title = title,
|
|
||||||
Status = TaskStatus.Manual,
|
|
||||||
CommitType = defaultCommitType,
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var taskRepo = new TaskRepository(context);
|
|
||||||
await taskRepo.AddAsync(entity);
|
|
||||||
var tags = await taskRepo.GetEffectiveTagsAsync(entity.Id);
|
|
||||||
var vm = new TaskItemViewModel(entity, tags, RunNowAsync, () => _worker.IsConnected,
|
|
||||||
_dbFactory, 0, ToggleDoneAsync, NotifySubtasksChanged);
|
|
||||||
Tasks.Add(vm);
|
|
||||||
SelectedTask = vm;
|
|
||||||
InlineAddTitle = "";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_showMessage($"Error creating task: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanAddTask))]
|
|
||||||
private async Task AddTask()
|
|
||||||
{
|
|
||||||
var listId = CurrentListId;
|
|
||||||
if (listId is null) return;
|
|
||||||
|
|
||||||
string defaultCommitType;
|
|
||||||
using (var context = _dbFactory.CreateDbContext())
|
|
||||||
{
|
|
||||||
var listRepo = new ListRepository(context);
|
|
||||||
var list = await listRepo.GetByIdAsync(listId);
|
|
||||||
defaultCommitType = list?.DefaultCommitType ?? "chore";
|
|
||||||
}
|
|
||||||
|
|
||||||
var editor = _editorFactory();
|
|
||||||
await editor.LoadAgentsAsync(_worker);
|
|
||||||
editor.InitForCreate(listId, defaultCommitType);
|
|
||||||
|
|
||||||
var window = new TaskEditorView { DataContext = editor };
|
|
||||||
editor.RequestClose += () => window.Close();
|
|
||||||
window.Closed += (_, _) => editor.OnWindowClosed();
|
|
||||||
_ = ShowDialogAsync(window);
|
|
||||||
|
|
||||||
var saved = await editor.ShowAndWaitAsync();
|
|
||||||
if (saved is null) return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var taskRepo = new TaskRepository(context);
|
|
||||||
var tagRepo = new TagRepository(context);
|
|
||||||
await taskRepo.AddAsync(saved);
|
|
||||||
|
|
||||||
foreach (var tagName in editor.SelectedTagNames)
|
|
||||||
{
|
|
||||||
var tagId = await tagRepo.GetOrCreateAsync(tagName);
|
|
||||||
await taskRepo.AddTagAsync(saved.Id, tagId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var tags = await taskRepo.GetEffectiveTagsAsync(saved.Id);
|
|
||||||
Tasks.Add(new TaskItemViewModel(saved, tags, RunNowAsync, () => _worker.IsConnected,
|
|
||||||
_dbFactory, 0, ToggleDoneAsync, NotifySubtasksChanged));
|
|
||||||
|
|
||||||
// Auto wake-queue if agent+queued
|
|
||||||
if (saved.Status == TaskStatus.Queued &&
|
|
||||||
tags.Any(t => t.Name == "agent"))
|
|
||||||
{
|
|
||||||
try { await _worker.WakeQueueAsync(); }
|
|
||||||
catch { /* worker offline is fine */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_showMessage($"Error creating task: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task EditTask()
|
|
||||||
{
|
|
||||||
if (SelectedTask is null || CurrentListId is null) return;
|
|
||||||
|
|
||||||
TaskEntity? entity;
|
|
||||||
List<TagEntity> taskTags;
|
|
||||||
using (var context = _dbFactory.CreateDbContext())
|
|
||||||
{
|
|
||||||
var taskRepo = new TaskRepository(context);
|
|
||||||
entity = await taskRepo.GetByIdAsync(SelectedTask.Id);
|
|
||||||
if (entity is null) return;
|
|
||||||
taskTags = await taskRepo.GetTagsAsync(entity.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
var editor = _editorFactory();
|
|
||||||
await editor.LoadAgentsAsync(_worker);
|
|
||||||
await editor.InitForEditAsync(entity, taskTags);
|
|
||||||
|
|
||||||
var window = new TaskEditorView { DataContext = editor };
|
|
||||||
editor.RequestClose += () => window.Close();
|
|
||||||
window.Closed += (_, _) => editor.OnWindowClosed();
|
|
||||||
_ = ShowDialogAsync(window);
|
|
||||||
|
|
||||||
var saved = await editor.ShowAndWaitAsync();
|
|
||||||
if (saved is null) return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var taskRepo = new TaskRepository(context);
|
|
||||||
var tagRepo = new TagRepository(context);
|
|
||||||
await taskRepo.UpdateAsync(saved);
|
|
||||||
|
|
||||||
var existingTags = await taskRepo.GetTagsAsync(saved.Id);
|
|
||||||
foreach (var old in existingTags)
|
|
||||||
await taskRepo.RemoveTagAsync(saved.Id, old.Id);
|
|
||||||
foreach (var tagName in editor.SelectedTagNames)
|
|
||||||
{
|
|
||||||
var tagId = await tagRepo.GetOrCreateAsync(tagName);
|
|
||||||
await taskRepo.AddTagAsync(saved.Id, tagId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var newTags = await taskRepo.GetEffectiveTagsAsync(saved.Id);
|
|
||||||
SelectedTask.Refresh(saved, newTags);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_showMessage($"Error updating task: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task DeleteTask()
|
|
||||||
{
|
|
||||||
if (SelectedTask is null) return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var taskRepo = new TaskRepository(context);
|
|
||||||
await taskRepo.DeleteAsync(SelectedTask.Id);
|
|
||||||
Tasks.Remove(SelectedTask);
|
|
||||||
SelectedTask = null;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_showMessage($"Error deleting task: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RefreshSingleAsync(string taskId)
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var taskRepo = new TaskRepository(context);
|
|
||||||
var entity = await taskRepo.GetByIdAsync(taskId);
|
|
||||||
var existing = Tasks.FirstOrDefault(t => t.Id == taskId);
|
|
||||||
if (entity is null)
|
|
||||||
{
|
|
||||||
if (existing is not null) Tasks.Remove(existing);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var tags = await taskRepo.GetEffectiveTagsAsync(taskId);
|
|
||||||
if (existing is not null)
|
|
||||||
{
|
|
||||||
existing.Refresh(entity, tags);
|
|
||||||
var subtaskCount = await context.Subtasks.CountAsync(s => s.TaskId == taskId);
|
|
||||||
await existing.RefreshSubtasksAsync(subtaskCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RunNowAsync(string taskId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _worker.RunNowAsync(taskId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_showMessage($"RunNow failed: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ToggleDoneAsync(string taskId)
|
|
||||||
{
|
|
||||||
using var context = _dbFactory.CreateDbContext();
|
|
||||||
var taskRepo = new TaskRepository(context);
|
|
||||||
var entity = await taskRepo.GetByIdAsync(taskId);
|
|
||||||
if (entity is null) return;
|
|
||||||
|
|
||||||
entity.Status = entity.Status == TaskStatus.Done ? TaskStatus.Manual : TaskStatus.Done;
|
|
||||||
if (entity.Status == TaskStatus.Done)
|
|
||||||
entity.FinishedAt = DateTime.UtcNow;
|
|
||||||
|
|
||||||
await taskRepo.UpdateAsync(entity);
|
|
||||||
await RefreshSingleAsync(taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnTaskUpdated(string taskId)
|
|
||||||
{
|
|
||||||
if (CurrentListId is null) return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await RefreshSingleAsync(taskId);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
System.Diagnostics.Debug.WriteLine($"[TaskListViewModel] OnTaskUpdated failed for {taskId}: {ex}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task ShowDialogAsync(Window dialog)
|
|
||||||
{
|
|
||||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop
|
|
||||||
&& desktop.MainWindow is not null)
|
|
||||||
{
|
|
||||||
await dialog.ShowDialog(desktop.MainWindow);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
dialog.Show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
|
|
||||||
xmlns:svc="using:ClaudeDo.Data.Models"
|
|
||||||
x:Class="ClaudeDo.Ui.Views.ListEditorView"
|
|
||||||
x:DataType="vm:ListEditorViewModel"
|
|
||||||
Title="{Binding WindowTitle}"
|
|
||||||
Width="450" Height="480"
|
|
||||||
WindowStartupLocation="CenterOwner"
|
|
||||||
CanResize="False"
|
|
||||||
Background="{StaticResource WindowBgBrush}">
|
|
||||||
<StackPanel Margin="16" Spacing="10">
|
|
||||||
<TextBlock Text="Name" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<TextBox Text="{Binding Name}" PlaceholderText="List name..."/>
|
|
||||||
|
|
||||||
<TextBlock Text="Working Directory" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<DockPanel>
|
|
||||||
<Button DockPanel.Dock="Right" Content="Browse..." Click="OnBrowseFolder" Margin="8,0,0,0" VerticalAlignment="Center"/>
|
|
||||||
<TextBox Text="{Binding WorkingDir}" PlaceholderText="(optional) Absolute path to git repo"/>
|
|
||||||
</DockPanel>
|
|
||||||
|
|
||||||
<TextBlock Text="Default Commit Type" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<ComboBox ItemsSource="{Binding CommitTypes}"
|
|
||||||
SelectedItem="{Binding DefaultCommitType}"
|
|
||||||
MinWidth="150"/>
|
|
||||||
|
|
||||||
<Border Height="1" Background="{StaticResource BorderSubtleBrush}" Margin="0,6,0,2"/>
|
|
||||||
|
|
||||||
<TextBlock Text="Agent Config" FontWeight="Bold" FontSize="13"
|
|
||||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
|
||||||
|
|
||||||
<TextBlock Text="Model" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<ComboBox ItemsSource="{Binding ModelDisplayNames}"
|
|
||||||
SelectedItem="{Binding Model}"
|
|
||||||
MinWidth="150"/>
|
|
||||||
|
|
||||||
<TextBlock Text="System Prompt" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<TextBox Text="{Binding SystemPrompt}"
|
|
||||||
PlaceholderText="(optional) Additional system instructions..."
|
|
||||||
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="50"/>
|
|
||||||
|
|
||||||
<TextBlock Text="Agent File" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<ComboBox ItemsSource="{Binding AvailableAgents}"
|
|
||||||
SelectedItem="{Binding SelectedAgent}"
|
|
||||||
MinWidth="150">
|
|
||||||
<ComboBox.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="svc:AgentInfo">
|
|
||||||
<TextBlock Text="{Binding Name}"/>
|
|
||||||
</DataTemplate>
|
|
||||||
</ComboBox.ItemTemplate>
|
|
||||||
</ComboBox>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right" Margin="0,10,0,0">
|
|
||||||
<Button Content="Save" Command="{Binding SaveCommand}" IsDefault="True" MinWidth="80"
|
|
||||||
Background="{StaticResource AccentBrush}" Foreground="{StaticResource TextPrimaryBrush}"/>
|
|
||||||
<Button Content="Cancel" Command="{Binding CancelCommand}" IsCancel="True" MinWidth="80"/>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Window>
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using Avalonia.Platform.Storage;
|
|
||||||
using ClaudeDo.Ui.ViewModels;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views;
|
|
||||||
|
|
||||||
public partial class ListEditorView : Window
|
|
||||||
{
|
|
||||||
public ListEditorView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnBrowseFolder(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var vm = DataContext as ListEditorViewModel;
|
|
||||||
var startPath = !string.IsNullOrWhiteSpace(vm?.WorkingDir) && Directory.Exists(vm.WorkingDir)
|
|
||||||
? vm.WorkingDir
|
|
||||||
: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
|
||||||
|
|
||||||
var startLocation = await StorageProvider.TryGetFolderFromPathAsync(new Uri(startPath));
|
|
||||||
|
|
||||||
var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
|
||||||
{
|
|
||||||
Title = "Select Working Directory",
|
|
||||||
SuggestedStartLocation = startLocation,
|
|
||||||
AllowMultiple = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.Count > 0)
|
|
||||||
{
|
|
||||||
var path = result[0].TryGetLocalPath();
|
|
||||||
if (path is not null && vm is not null)
|
|
||||||
vm.WorkingDir = path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
|
|
||||||
xmlns:conv="using:ClaudeDo.Ui.Converters"
|
|
||||||
x:Class="ClaudeDo.Ui.Views.StatusBarView"
|
|
||||||
x:DataType="vm:StatusBarViewModel">
|
|
||||||
<Border Background="#222" Padding="6,3">
|
|
||||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto">
|
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6">
|
|
||||||
<Ellipse Width="10" Height="10" VerticalAlignment="Center"
|
|
||||||
Fill="{Binding ConnectionStatus, Converter={x:Static conv:ConnectionColorConverter.Instance}}"/>
|
|
||||||
<TextBlock Text="{Binding ConnectionStatus}" Foreground="White" VerticalAlignment="Center" FontSize="12"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<TextBlock Grid.Column="1" Text="{Binding ActiveTasksSummary}" Foreground="LightGray"
|
|
||||||
VerticalAlignment="Center" Margin="20,0,0,0" FontSize="12"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Column="3" Text="{Binding StatusMessage}" Foreground="Yellow"
|
|
||||||
VerticalAlignment="Center" FontSize="12" Margin="0,0,8,0"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views;
|
|
||||||
|
|
||||||
public partial class StatusBarView : UserControl
|
|
||||||
{
|
|
||||||
public StatusBarView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
|
|
||||||
xmlns:conv="using:ClaudeDo.Ui.Converters"
|
|
||||||
xmlns:m="using:ClaudeDo.Data.Models"
|
|
||||||
x:Class="ClaudeDo.Ui.Views.TaskDetailView"
|
|
||||||
x:DataType="vm:TaskDetailViewModel">
|
|
||||||
<ScrollViewer>
|
|
||||||
<StackPanel Margin="12" Spacing="8"
|
|
||||||
IsVisible="{Binding Title, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
|
||||||
|
|
||||||
<!-- Title (large, editable) -->
|
|
||||||
<TextBox x:Name="TitleBox"
|
|
||||||
Text="{Binding Title}"
|
|
||||||
FontWeight="Bold" FontSize="16"
|
|
||||||
Foreground="{StaticResource TextPrimaryBrush}"
|
|
||||||
BorderThickness="0" Background="Transparent"
|
|
||||||
Padding="0,4"
|
|
||||||
LostFocus="OnFieldLostFocus"/>
|
|
||||||
|
|
||||||
<!-- Status + Commit Type row -->
|
|
||||||
<Grid ColumnDefinitions="*,16,*" Margin="0,4,0,0">
|
|
||||||
<StackPanel Grid.Column="0" Spacing="4">
|
|
||||||
<TextBlock Text="Status" FontSize="12" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<ComboBox ItemsSource="{Binding StatusChoices}"
|
|
||||||
SelectedItem="{Binding StatusChoice}"
|
|
||||||
MinWidth="100"
|
|
||||||
LostFocus="OnFieldLostFocus"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Grid.Column="2" Spacing="4">
|
|
||||||
<TextBlock Text="Commit Type" FontSize="12" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<ComboBox ItemsSource="{Binding CommitTypes}"
|
|
||||||
SelectedItem="{Binding CommitType}"
|
|
||||||
MinWidth="100"
|
|
||||||
LostFocus="OnFieldLostFocus"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Tags -->
|
|
||||||
<StackPanel Spacing="4" Margin="0,8,0,0">
|
|
||||||
<TextBlock Text="Tags" FontSize="12" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<WrapPanel Orientation="Horizontal">
|
|
||||||
<ItemsControl ItemsSource="{Binding Tags}">
|
|
||||||
<ItemsControl.ItemsPanel>
|
|
||||||
<ItemsPanelTemplate>
|
|
||||||
<WrapPanel Orientation="Horizontal"/>
|
|
||||||
</ItemsPanelTemplate>
|
|
||||||
</ItemsControl.ItemsPanel>
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="m:TagEntity">
|
|
||||||
<Border CornerRadius="10" Padding="8,3" Margin="0,0,4,4"
|
|
||||||
Background="{StaticResource AccentSubtleBrush}">
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
|
||||||
<TextBlock Text="{Binding Name}" FontSize="12"
|
|
||||||
Foreground="{StaticResource AccentLightBrush}"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
<Button Content="x" FontSize="10" Padding="2,0"
|
|
||||||
Background="Transparent" BorderThickness="0"
|
|
||||||
Foreground="{StaticResource TextMutedBrush}"
|
|
||||||
Cursor="Hand"
|
|
||||||
Command="{Binding $parent[UserControl].((vm:TaskDetailViewModel)DataContext).RemoveTagCommand}"
|
|
||||||
CommandParameter="{Binding}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
<TextBox Text="{Binding NewTagInput}"
|
|
||||||
PlaceholderText="Add tag..."
|
|
||||||
Width="100" FontSize="12"
|
|
||||||
BorderThickness="0" Background="Transparent"
|
|
||||||
Padding="4,3"
|
|
||||||
KeyDown="OnTagInputKeyDown"/>
|
|
||||||
</WrapPanel>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Description (editable) -->
|
|
||||||
<TextBlock Text="Description" FontSize="12" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,8,0,2"/>
|
|
||||||
<TextBox Text="{Binding Description}"
|
|
||||||
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="60"
|
|
||||||
Foreground="{StaticResource TextPrimaryBrush}"
|
|
||||||
PlaceholderText="Add a description..."
|
|
||||||
LostFocus="OnFieldLostFocus"/>
|
|
||||||
|
|
||||||
<!-- Sub-Tasks -->
|
|
||||||
<Border Height="1" Background="{StaticResource BorderSubtleBrush}" Margin="0,8,0,4"/>
|
|
||||||
<TextBlock Text="Sub-Tasks" FontWeight="Bold" FontSize="13"
|
|
||||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
|
||||||
<ItemsControl ItemsSource="{Binding Subtasks}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="vm:SubtaskItemViewModel">
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,2,0,2">
|
|
||||||
<CheckBox IsChecked="{Binding Completed}" VerticalAlignment="Center"/>
|
|
||||||
<TextBox Text="{Binding Title}" PlaceholderText="Subtask title..." Width="220"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
LostFocus="OnSubtaskTitleLostFocus"/>
|
|
||||||
<Button Content="✕" Padding="6,2"
|
|
||||||
Background="Transparent" BorderThickness="0"
|
|
||||||
Foreground="{StaticResource TextMutedBrush}"
|
|
||||||
Command="{Binding $parent[UserControl].((vm:TaskDetailViewModel)DataContext).RemoveSubtaskCommand}"
|
|
||||||
CommandParameter="{Binding}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
<Button Content="+ Add Sub-Task" Command="{Binding AddSubtaskCommand}"
|
|
||||||
Background="Transparent" BorderThickness="0"
|
|
||||||
Foreground="{StaticResource AccentLightBrush}" HorizontalAlignment="Left"/>
|
|
||||||
|
|
||||||
<!-- Agent Config (overrides) -->
|
|
||||||
<Border Height="1" Background="{StaticResource BorderSubtleBrush}" Margin="0,8,0,4"/>
|
|
||||||
<TextBlock Text="Agent Config (overrides)" FontWeight="Bold" FontSize="13"
|
|
||||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,12,*" Margin="0,4,0,0">
|
|
||||||
<StackPanel Grid.Column="0" Spacing="4">
|
|
||||||
<TextBlock Text="Model" FontSize="12" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<ComboBox ItemsSource="{Binding ModelChoices}"
|
|
||||||
SelectedItem="{Binding ModelChoice}"
|
|
||||||
MinWidth="100"
|
|
||||||
LostFocus="OnFieldLostFocus"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Grid.Column="2" Spacing="4">
|
|
||||||
<TextBlock Text="Agent File" FontSize="12" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
|
||||||
<ComboBox ItemsSource="{Binding AvailableAgents}"
|
|
||||||
SelectedItem="{Binding SelectedAgent}"
|
|
||||||
MinWidth="100"
|
|
||||||
LostFocus="OnFieldLostFocus">
|
|
||||||
<ComboBox.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="m:AgentInfo">
|
|
||||||
<TextBlock Text="{Binding Name}"/>
|
|
||||||
</DataTemplate>
|
|
||||||
</ComboBox.ItemTemplate>
|
|
||||||
</ComboBox>
|
|
||||||
<Button Content="…" Click="OnBrowseAgent" ToolTip.Tip="Browse for agent .md file"/>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<TextBlock Text="System Prompt" FontSize="12" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,2"/>
|
|
||||||
<TextBox Text="{Binding SystemPromptOverride}"
|
|
||||||
PlaceholderText="(inherits from list)"
|
|
||||||
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="50"
|
|
||||||
LostFocus="OnFieldLostFocus"/>
|
|
||||||
|
|
||||||
<!-- === READ-ONLY ZONE === -->
|
|
||||||
|
|
||||||
<TextBlock Text="Result" FontWeight="SemiBold" FontSize="12"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,12,0,2"/>
|
|
||||||
<TextBox Text="{Binding Result, Mode=OneWay}" IsReadOnly="True"
|
|
||||||
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="60"
|
|
||||||
IsVisible="{Binding Result, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
|
||||||
<TextBlock Text="(no result yet)" Foreground="{StaticResource TextMutedBrush}" FontStyle="Italic"
|
|
||||||
IsVisible="{Binding Result, Converter={x:Static StringConverters.IsNullOrEmpty}}"/>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="4"
|
|
||||||
IsVisible="{Binding LogPath, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
|
||||||
<TextBlock Text="Log:" FontWeight="SemiBold" FontSize="12"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}" VerticalAlignment="Center"/>
|
|
||||||
<TextBlock Text="{Binding LogPath}" FontSize="11"
|
|
||||||
Foreground="{StaticResource TextDimBrush}" VerticalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<TextBlock Text="Live Output" FontWeight="SemiBold" FontSize="12"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,8,0,2"/>
|
|
||||||
<TextBox x:Name="LiveOutputBox"
|
|
||||||
Text="{Binding LiveText, Mode=OneWay}"
|
|
||||||
IsReadOnly="True"
|
|
||||||
AcceptsReturn="True"
|
|
||||||
TextWrapping="NoWrap"
|
|
||||||
FontFamily="Consolas,Courier New,monospace"
|
|
||||||
FontSize="11"
|
|
||||||
MaxHeight="300"
|
|
||||||
Foreground="{StaticResource TextPrimaryBrush}"
|
|
||||||
BorderBrush="{StaticResource BorderSubtleBrush}"
|
|
||||||
BorderThickness="1"
|
|
||||||
CornerRadius="6"
|
|
||||||
Padding="6"/>
|
|
||||||
|
|
||||||
<Border IsVisible="{Binding HasWorktree}" BorderBrush="{StaticResource AccentBrush}"
|
|
||||||
BorderThickness="1" CornerRadius="8" Padding="10" Margin="0,8,0,0">
|
|
||||||
<StackPanel Spacing="6">
|
|
||||||
<TextBlock Text="Worktree" FontWeight="Bold" FontSize="14"
|
|
||||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
|
||||||
<TextBlock Text="Branch:" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<TextBlock Text="{Binding BranchName}" FontFamily="Consolas,Courier New,monospace"
|
|
||||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
|
||||||
<TextBlock Text="State:" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<TextBlock Text="{Binding WorktreeState}"
|
|
||||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<TextBlock Text="Diff Stat:" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"
|
|
||||||
IsVisible="{Binding DiffStat, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
|
||||||
<TextBox Text="{Binding DiffStat, Mode=OneWay}" IsReadOnly="True"
|
|
||||||
AcceptsReturn="True" FontFamily="Consolas,Courier New,monospace" FontSize="11"
|
|
||||||
IsVisible="{Binding DiffStat, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
|
||||||
<WrapPanel Orientation="Horizontal" Margin="0,4,0,0">
|
|
||||||
<Button Content="Open Worktree" Command="{Binding OpenWorktreeCommand}" Margin="0,0,4,4"/>
|
|
||||||
<Button Content="Show Diff" Command="{Binding ShowDiffCommand}" Margin="0,0,4,4"/>
|
|
||||||
<Button Content="Merge into main" Command="{Binding MergeIntoMainCommand}"
|
|
||||||
IsEnabled="{Binding CanWorktreeAction}" Margin="0,0,4,4"/>
|
|
||||||
<Button Content="Keep as branch" Command="{Binding KeepAsBranchCommand}"
|
|
||||||
IsEnabled="{Binding CanWorktreeAction}" Margin="0,0,4,4"/>
|
|
||||||
<Button Content="Discard" Command="{Binding DiscardCommand}"
|
|
||||||
IsEnabled="{Binding CanWorktreeAction}" Margin="0,0,4,4"/>
|
|
||||||
</WrapPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Input;
|
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using Avalonia.Platform.Storage;
|
|
||||||
using ClaudeDo.Ui.ViewModels;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views;
|
|
||||||
|
|
||||||
public partial class TaskDetailView : UserControl
|
|
||||||
{
|
|
||||||
public TaskDetailView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnFieldLostFocus(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is TaskDetailViewModel vm)
|
|
||||||
await vm.SaveAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSubtaskTitleLostFocus(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
// Title change is handled by SubtaskItemViewModel.PropertyChanged → OnSubtaskPropertyChanged in the VM
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnBrowseAgent(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var topLevel = TopLevel.GetTopLevel(this);
|
|
||||||
if (topLevel is null) return;
|
|
||||||
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
|
||||||
{
|
|
||||||
Title = "Select Agent File",
|
|
||||||
AllowMultiple = false,
|
|
||||||
FileTypeFilter = new[] { new FilePickerFileType("Agent Files") { Patterns = ["*.md"] } },
|
|
||||||
});
|
|
||||||
if (files.Count == 0) return;
|
|
||||||
var path = files[0].TryGetLocalPath();
|
|
||||||
if (path is null) return;
|
|
||||||
if (DataContext is TaskDetailViewModel vm)
|
|
||||||
{
|
|
||||||
vm.SetAgentFromPath(path);
|
|
||||||
await vm.SaveAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTagInputKeyDown(object? sender, KeyEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.Key == Key.Enter && DataContext is TaskDetailViewModel vm)
|
|
||||||
{
|
|
||||||
vm.AddTagCommand.Execute(null);
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void FocusTitle()
|
|
||||||
{
|
|
||||||
this.FindControl<TextBox>("TitleBox")?.Focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private TaskDetailViewModel? _previousVm;
|
|
||||||
|
|
||||||
protected override void OnDataContextChanged(EventArgs e)
|
|
||||||
{
|
|
||||||
base.OnDataContextChanged(e);
|
|
||||||
if (_previousVm is not null)
|
|
||||||
_previousVm.PropertyChanged -= OnViewModelPropertyChanged;
|
|
||||||
_previousVm = DataContext as TaskDetailViewModel;
|
|
||||||
if (_previousVm is not null)
|
|
||||||
_previousVm.PropertyChanged += OnViewModelPropertyChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.PropertyName == nameof(TaskDetailViewModel.LiveText))
|
|
||||||
{
|
|
||||||
var box = this.FindControl<TextBox>("LiveOutputBox");
|
|
||||||
if (box is not null)
|
|
||||||
{
|
|
||||||
box.CaretIndex = box.Text?.Length ?? 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
|
|
||||||
xmlns:models="using:ClaudeDo.Data.Models"
|
|
||||||
x:Class="ClaudeDo.Ui.Views.TaskEditorView"
|
|
||||||
x:DataType="vm:TaskEditorViewModel"
|
|
||||||
Title="{Binding WindowTitle}"
|
|
||||||
Width="500" Height="600"
|
|
||||||
WindowStartupLocation="CenterOwner"
|
|
||||||
CanResize="False"
|
|
||||||
Background="{StaticResource WindowBgBrush}">
|
|
||||||
<StackPanel Margin="16" Spacing="10">
|
|
||||||
<TextBlock Text="Title" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<TextBox Text="{Binding Title}" PlaceholderText="Task title..."/>
|
|
||||||
|
|
||||||
<TextBlock Text="Description" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<TextBox Text="{Binding Description}" PlaceholderText="(optional)" AcceptsReturn="True"
|
|
||||||
TextWrapping="Wrap" MinHeight="80"/>
|
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,16,*">
|
|
||||||
<StackPanel Grid.Column="0" Spacing="4">
|
|
||||||
<TextBlock Text="Status" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<ComboBox ItemsSource="{Binding StatusChoices}"
|
|
||||||
SelectedItem="{Binding StatusChoice}"
|
|
||||||
MinWidth="120"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Grid.Column="2" Spacing="4">
|
|
||||||
<TextBlock Text="Commit Type" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<ComboBox ItemsSource="{Binding CommitTypes}"
|
|
||||||
SelectedItem="{Binding CommitType}"
|
|
||||||
MinWidth="120"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<TextBlock Text="Tags (comma-separated)" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<TextBox Text="{Binding TagsInput}" PlaceholderText="agent, manual, code, ..."/>
|
|
||||||
|
|
||||||
<!-- Sub-Tasks -->
|
|
||||||
<Border Height="1" Background="{StaticResource BorderSubtleBrush}" Margin="0,6,0,2"/>
|
|
||||||
<TextBlock Text="Sub-Tasks" FontWeight="Bold" FontSize="13"
|
|
||||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
|
||||||
<ItemsControl ItemsSource="{Binding Subtasks}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="vm:SubtaskItemViewModel">
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,2,0,2">
|
|
||||||
<CheckBox IsChecked="{Binding Completed}" VerticalAlignment="Center"/>
|
|
||||||
<TextBox Text="{Binding Title}" PlaceholderText="Subtask title..." Width="320"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
<Button Content="✕" Padding="6,2"
|
|
||||||
Background="Transparent" BorderThickness="0"
|
|
||||||
Foreground="{StaticResource TextMutedBrush}"
|
|
||||||
Command="{Binding $parent[Window].((vm:TaskEditorViewModel)DataContext).RemoveSubtaskCommand}"
|
|
||||||
CommandParameter="{Binding}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
<Button Content="+ Add Sub-Task" Command="{Binding AddSubtaskCommand}"
|
|
||||||
Background="Transparent" BorderThickness="0"
|
|
||||||
Foreground="{StaticResource AccentLightBrush}" HorizontalAlignment="Left"/>
|
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<Border Height="1" Background="{StaticResource BorderSubtleBrush}" Margin="0,6,0,2"/>
|
|
||||||
|
|
||||||
<TextBlock Text="Agent Config (overrides)" FontWeight="Bold" FontSize="13"
|
|
||||||
Foreground="{StaticResource TextPrimaryBrush}"/>
|
|
||||||
|
|
||||||
<TextBlock Text="Model" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<ComboBox ItemsSource="{Binding ModelChoices}"
|
|
||||||
SelectedItem="{Binding ModelChoice}"
|
|
||||||
MinWidth="150"/>
|
|
||||||
|
|
||||||
<TextBlock Text="System Prompt" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<TextBox Text="{Binding SystemPromptOverride}"
|
|
||||||
PlaceholderText="(inherits from list)"
|
|
||||||
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="50"/>
|
|
||||||
|
|
||||||
<TextBlock Text="Agent File" FontWeight="SemiBold"
|
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
||||||
<ComboBox ItemsSource="{Binding AvailableAgents}"
|
|
||||||
SelectedItem="{Binding SelectedAgent}"
|
|
||||||
MinWidth="150">
|
|
||||||
<ComboBox.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="models:AgentInfo">
|
|
||||||
<TextBlock Text="{Binding Name}"/>
|
|
||||||
</DataTemplate>
|
|
||||||
</ComboBox.ItemTemplate>
|
|
||||||
</ComboBox>
|
|
||||||
<Button Content="…" Click="OnBrowseAgent" ToolTip.Tip="Browse for agent .md file"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right" Margin="0,10,0,0">
|
|
||||||
<Button Content="Save" Command="{Binding SaveCommand}" IsDefault="True" MinWidth="80"
|
|
||||||
Background="{StaticResource AccentBrush}" Foreground="{StaticResource TextPrimaryBrush}"/>
|
|
||||||
<Button Content="Cancel" Command="{Binding CancelCommand}" IsCancel="True" MinWidth="80"/>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Window>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using Avalonia.Platform.Storage;
|
|
||||||
using ClaudeDo.Ui.ViewModels;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views;
|
|
||||||
|
|
||||||
public partial class TaskEditorView : Window
|
|
||||||
{
|
|
||||||
public TaskEditorView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnBrowseAgent(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
|
||||||
{
|
|
||||||
Title = "Select Agent File",
|
|
||||||
AllowMultiple = false,
|
|
||||||
FileTypeFilter = new[] { new FilePickerFileType("Agent Files") { Patterns = ["*.md"] } },
|
|
||||||
});
|
|
||||||
if (files.Count == 0) return;
|
|
||||||
var path = files[0].TryGetLocalPath();
|
|
||||||
if (path is null) return;
|
|
||||||
if (DataContext is TaskEditorViewModel vm)
|
|
||||||
vm.SetAgentFromPath(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
|
|
||||||
xmlns:conv="using:ClaudeDo.Ui.Converters"
|
|
||||||
x:Class="ClaudeDo.Ui.Views.TaskListView"
|
|
||||||
x:DataType="vm:TaskListViewModel"
|
|
||||||
x:Name="Root">
|
|
||||||
<DockPanel>
|
|
||||||
<!-- Inline add field at bottom -->
|
|
||||||
<Border DockPanel.Dock="Bottom" Padding="8,8"
|
|
||||||
BorderThickness="0,1,0,0" BorderBrush="{StaticResource BorderSubtleBrush}">
|
|
||||||
<TextBox x:Name="InlineAddBox"
|
|
||||||
Text="{Binding InlineAddTitle, Mode=TwoWay}"
|
|
||||||
PlaceholderText="+ Add a task..."
|
|
||||||
BorderThickness="1"
|
|
||||||
BorderBrush="{StaticResource BorderSubtleBrush}"
|
|
||||||
CornerRadius="8"
|
|
||||||
Padding="10,8"
|
|
||||||
FontSize="13"
|
|
||||||
KeyDown="OnInlineAddKeyDown"
|
|
||||||
GotFocus="OnInlineAddGotFocus"
|
|
||||||
LostFocus="OnInlineAddLostFocus"/>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Task list -->
|
|
||||||
<ListBox x:Name="TaskListBox"
|
|
||||||
ItemsSource="{Binding Tasks}"
|
|
||||||
SelectedItem="{Binding SelectedTask}"
|
|
||||||
Background="Transparent"
|
|
||||||
Margin="4,0"
|
|
||||||
KeyDown="OnTaskListKeyDown">
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="vm:TaskItemViewModel">
|
|
||||||
<Grid RowDefinitions="Auto,Auto"
|
|
||||||
Background="Transparent"
|
|
||||||
Opacity="{Binding RowOpacity}">
|
|
||||||
<!-- Row 0: Task row -->
|
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="20,Auto,*" Margin="4,4"
|
|
||||||
DoubleTapped="OnTaskItemDoubleTapped"
|
|
||||||
PointerPressed="OnTaskItemPointerPressed">
|
|
||||||
<Grid.ContextFlyout>
|
|
||||||
<MenuFlyout>
|
|
||||||
<MenuItem Header="Edit"
|
|
||||||
Command="{Binding #Root.((vm:TaskListViewModel)DataContext).EditTaskCommand}"/>
|
|
||||||
<MenuItem Header="Delete"
|
|
||||||
Command="{Binding #Root.((vm:TaskListViewModel)DataContext).DeleteTaskCommand}"/>
|
|
||||||
<Separator/>
|
|
||||||
<MenuItem Header="Run Now"
|
|
||||||
Command="{Binding RunNowCommand}"/>
|
|
||||||
</MenuFlyout>
|
|
||||||
</Grid.ContextFlyout>
|
|
||||||
|
|
||||||
<!-- Expand/collapse chevron -->
|
|
||||||
<Button Grid.Column="0"
|
|
||||||
Command="{Binding ToggleExpandedCommand}"
|
|
||||||
IsVisible="{Binding HasSubtasks}"
|
|
||||||
Background="Transparent"
|
|
||||||
BorderThickness="0"
|
|
||||||
Padding="0"
|
|
||||||
Width="16" Height="16"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Cursor="Hand">
|
|
||||||
<Panel>
|
|
||||||
<Canvas Width="10" Height="10"
|
|
||||||
IsVisible="{Binding !IsExpanded}">
|
|
||||||
<Path Stroke="{StaticResource TextDimBrush}" StrokeThickness="1.5"
|
|
||||||
Data="M 2,0 L 8,5 L 2,10"/>
|
|
||||||
</Canvas>
|
|
||||||
<Canvas Width="10" Height="10"
|
|
||||||
IsVisible="{Binding IsExpanded}">
|
|
||||||
<Path Stroke="{StaticResource TextDimBrush}" StrokeThickness="1.5"
|
|
||||||
Data="M 0,2 L 5,8 L 10,2"/>
|
|
||||||
</Canvas>
|
|
||||||
</Panel>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<!-- Circular checkbox -->
|
|
||||||
<Border Grid.Column="1" Width="22" Height="22"
|
|
||||||
CornerRadius="11"
|
|
||||||
BorderThickness="2"
|
|
||||||
BorderBrush="{Binding StatusText, Converter={x:Static conv:CheckboxBorderConverter.Instance}}"
|
|
||||||
Background="Transparent"
|
|
||||||
VerticalAlignment="Center" Margin="0,0,10,0"
|
|
||||||
Cursor="Hand"
|
|
||||||
PointerPressed="OnCheckboxPressed">
|
|
||||||
<Panel>
|
|
||||||
<Canvas Width="12" Height="12"
|
|
||||||
IsVisible="{Binding IsDone}"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
|
||||||
<Path Stroke="{StaticResource StatusGreenBrush}" StrokeThickness="2"
|
|
||||||
Data="M 1,6 L 4.5,9.5 L 11,3"/>
|
|
||||||
</Canvas>
|
|
||||||
<Ellipse Width="8" Height="8"
|
|
||||||
Fill="{StaticResource StatusOrangeBrush}"
|
|
||||||
IsVisible="{Binding IsRunning}"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
|
||||||
<Ellipse Width="8" Height="8" Fill="#FFD700"
|
|
||||||
IsVisible="{Binding IsStarting}"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
|
||||||
</Panel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Task content -->
|
|
||||||
<StackPanel Grid.Column="2" VerticalAlignment="Center">
|
|
||||||
<TextBlock Text="{Binding Title}" FontWeight="Medium"
|
|
||||||
Foreground="{Binding TitleForeground}"
|
|
||||||
TextDecorations="{Binding TitleDecorations}"
|
|
||||||
TextTrimming="CharacterEllipsis"/>
|
|
||||||
<TextBlock FontSize="11"
|
|
||||||
Foreground="{StaticResource TextDimBrush}"
|
|
||||||
IsVisible="{Binding TagsText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
|
||||||
<TextBlock.Text>
|
|
||||||
<MultiBinding StringFormat="{}{0} · {1}">
|
|
||||||
<Binding Path="TagsText"/>
|
|
||||||
<Binding Path="StatusText"/>
|
|
||||||
</MultiBinding>
|
|
||||||
</TextBlock.Text>
|
|
||||||
</TextBlock>
|
|
||||||
<TextBlock Text="{Binding StatusText}" FontSize="11"
|
|
||||||
Foreground="{StaticResource TextDimBrush}"
|
|
||||||
IsVisible="{Binding TagsText, Converter={x:Static StringConverters.IsNullOrEmpty}}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Row 1: Subtask list (visible when expanded) -->
|
|
||||||
<ItemsControl Grid.Row="1"
|
|
||||||
ItemsSource="{Binding Subtasks}"
|
|
||||||
IsVisible="{Binding IsExpanded}"
|
|
||||||
Margin="40,0,0,4">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="vm:SubtaskItemViewModel">
|
|
||||||
<Grid ColumnDefinitions="Auto,*" Margin="0,2"
|
|
||||||
PointerPressed="OnSubtaskPointerPressed">
|
|
||||||
<Grid.ContextFlyout>
|
|
||||||
<MenuFlyout>
|
|
||||||
<MenuItem Header="Edit Task"
|
|
||||||
Command="{Binding #Root.((vm:TaskListViewModel)DataContext).EditTaskCommand}"/>
|
|
||||||
</MenuFlyout>
|
|
||||||
</Grid.ContextFlyout>
|
|
||||||
<CheckBox Grid.Column="0"
|
|
||||||
IsChecked="{Binding Completed, Mode=OneWay}"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="0,0,6,0"
|
|
||||||
MinWidth="0"
|
|
||||||
Click="OnSubtaskCheckboxClick"/>
|
|
||||||
<TextBlock Grid.Column="1"
|
|
||||||
Text="{Binding Title}"
|
|
||||||
FontSize="12"
|
|
||||||
Foreground="{StaticResource TextDimBrush}"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
TextTrimming="CharacterEllipsis"/>
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
</DockPanel>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Input;
|
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using Avalonia.VisualTree;
|
|
||||||
using ClaudeDo.Ui.ViewModels;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views;
|
|
||||||
|
|
||||||
public partial class TaskListView : UserControl
|
|
||||||
{
|
|
||||||
public TaskListView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnInlineAddKeyDown(object? sender, KeyEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is not TaskListViewModel vm) return;
|
|
||||||
|
|
||||||
if (e.Key == Key.Enter)
|
|
||||||
{
|
|
||||||
vm.InlineAddCommand.Execute(null);
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
else if (e.Key == Key.Escape)
|
|
||||||
{
|
|
||||||
vm.InlineAddTitle = "";
|
|
||||||
this.FindControl<ListBox>("TaskListBox")?.Focus();
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnInlineAddGotFocus(object? sender, FocusChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is TextBox tb)
|
|
||||||
tb.BorderBrush = Avalonia.Application.Current?.FindResource("AccentBrush") as Avalonia.Media.IBrush;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnInlineAddLostFocus(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is TextBox tb)
|
|
||||||
tb.BorderBrush = Avalonia.Application.Current?.FindResource("BorderSubtleBrush") as Avalonia.Media.IBrush;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTaskListKeyDown(object? sender, KeyEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is not TaskListViewModel vm || vm.SelectedTask is null) return;
|
|
||||||
|
|
||||||
switch (e.Key)
|
|
||||||
{
|
|
||||||
case Key.Delete:
|
|
||||||
vm.DeleteTaskCommand.Execute(null);
|
|
||||||
e.Handled = true;
|
|
||||||
break;
|
|
||||||
case Key.Space:
|
|
||||||
if (vm.SelectedTask.CanToggleDone)
|
|
||||||
{
|
|
||||||
vm.SelectedTask.ToggleDoneCommand.Execute(null);
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Key.Enter:
|
|
||||||
case Key.F2:
|
|
||||||
var detailView = this.GetVisualAncestors().OfType<Window>().FirstOrDefault()
|
|
||||||
?.GetVisualDescendants().OfType<TaskDetailView>().FirstOrDefault();
|
|
||||||
detailView?.FocusTitle();
|
|
||||||
e.Handled = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCheckboxPressed(object? sender, PointerPressedEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is not Border { DataContext: TaskItemViewModel task }) return;
|
|
||||||
if (task.CanToggleDone)
|
|
||||||
{
|
|
||||||
task.ToggleDoneCommand.Execute(null);
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTaskItemDoubleTapped(object? sender, TappedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is TaskListViewModel vm)
|
|
||||||
vm.EditTaskCommand.Execute(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTaskItemPointerPressed(object? sender, PointerPressedEventArgs e)
|
|
||||||
{
|
|
||||||
var props = e.GetCurrentPoint(this).Properties;
|
|
||||||
if (!props.IsRightButtonPressed) return;
|
|
||||||
|
|
||||||
if (sender is Grid { DataContext: TaskItemViewModel item }
|
|
||||||
&& DataContext is TaskListViewModel vm)
|
|
||||||
{
|
|
||||||
vm.SelectedTask = item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSubtaskPointerPressed(object? sender, PointerPressedEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.GetCurrentPoint(null).Properties.IsRightButtonPressed
|
|
||||||
&& sender is Control { DataContext: SubtaskItemViewModel subtask }
|
|
||||||
&& DataContext is TaskListViewModel vm)
|
|
||||||
{
|
|
||||||
var parent = vm.Tasks.FirstOrDefault(t => t.Subtasks.Contains(subtask));
|
|
||||||
if (parent is not null)
|
|
||||||
vm.SelectedTask = parent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnSubtaskCheckboxClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is CheckBox { DataContext: SubtaskItemViewModel subtask }
|
|
||||||
&& DataContext is TaskListViewModel vm)
|
|
||||||
{
|
|
||||||
var parent = vm.Tasks.FirstOrDefault(t => t.Subtasks.Contains(subtask));
|
|
||||||
if (parent is not null)
|
|
||||||
await parent.ToggleSubtaskDoneCommand.ExecuteAsync(subtask.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void FocusInlineAdd()
|
|
||||||
{
|
|
||||||
this.FindControl<TextBox>("InlineAddBox")?.Focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user