diff --git a/src/ClaudeDo.App/App.axaml b/src/ClaudeDo.App/App.axaml
index c2f87da..a2cce75 100644
--- a/src/ClaudeDo.App/App.axaml
+++ b/src/ClaudeDo.App/App.axaml
@@ -2,14 +2,49 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ClaudeDo.App.App"
xmlns:local="using:ClaudeDo.App"
- RequestedThemeVariant="Default">
-
+ RequestedThemeVariant="Dark">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
\ No newline at end of file
diff --git a/src/ClaudeDo.App/Program.cs b/src/ClaudeDo.App/Program.cs
index e5842db..7a5ce37 100644
--- a/src/ClaudeDo.App/Program.cs
+++ b/src/ClaudeDo.App/Program.cs
@@ -60,7 +60,13 @@ sealed class Program
sc.AddTransient();
sc.AddTransient();
sc.AddSingleton();
- sc.AddSingleton();
+ sc.AddSingleton(sp => new TaskDetailViewModel(
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ sp.GetRequiredService()));
sc.AddSingleton(sp =>
{
var taskRepo = sp.GetRequiredService();
diff --git a/src/ClaudeDo.Ui/Converters/CheckboxBorderConverter.cs b/src/ClaudeDo.Ui/Converters/CheckboxBorderConverter.cs
new file mode 100644
index 0000000..9e9e20f
--- /dev/null
+++ b/src/ClaudeDo.Ui/Converters/CheckboxBorderConverter.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+
+namespace ClaudeDo.Ui.Converters;
+
+public sealed class CheckboxBorderConverter : IValueConverter
+{
+ public static readonly CheckboxBorderConverter Instance = new();
+
+ private static readonly ISolidColorBrush Gray = new SolidColorBrush(Color.Parse("#475569"));
+ private static readonly ISolidColorBrush Orange = new SolidColorBrush(Color.Parse("#e67e22"));
+ private static readonly ISolidColorBrush Green = new SolidColorBrush(Color.Parse("#3d9474"));
+ private static readonly ISolidColorBrush Red = new SolidColorBrush(Color.Parse("#ef4444"));
+
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ return value?.ToString()?.ToLowerInvariant() switch
+ {
+ "running" => Orange,
+ "done" => Green,
+ "failed" => Red,
+ _ => Gray, // manual, queued
+ };
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ => throw new NotSupportedException();
+}
diff --git a/src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs b/src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs
index 889a94c..8be03d5 100644
--- a/src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs
@@ -1,3 +1,5 @@
+using System;
+using Avalonia.Media;
using ClaudeDo.Data.Models;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -9,6 +11,17 @@ public partial class ListItemViewModel : ViewModelBase
[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)
diff --git a/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs
index dc853dd..4b3d573 100644
--- a/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs
@@ -41,6 +41,7 @@ public partial class MainWindowViewModel : ViewModelBase
StatusBar = statusBar;
TaskList.SelectedTaskChanged += OnSelectedTaskChanged;
+ TaskDetail.TaskChanged += taskId => _ = TaskList.RefreshSingleAsync(taskId);
}
public async Task InitializeAsync()
diff --git a/src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs b/src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs
index ce09b6d..c6959f6 100644
--- a/src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs
@@ -16,12 +16,18 @@ public partial class TaskDetailViewModel : ViewModelBase
private readonly ListRepository _listRepo;
private readonly GitService _git;
private readonly WorkerClient _worker;
+ private readonly TagRepository _tagRepo;
[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";
+
+ 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"];
// Worktree
[ObservableProperty] private bool _hasWorktree;
@@ -32,19 +38,25 @@ public partial class TaskDetailViewModel : ViewModelBase
// Live stream
public ObservableCollection LiveLines { get; } = new();
+ public ObservableCollection Tags { get; } = new();
+ [ObservableProperty] private string _newTagInput = "";
private string? _taskId;
private string? _listId;
+ private bool _isLoading;
private const int MaxLiveLines = 500;
+ public event Action? TaskChanged;
+
public TaskDetailViewModel(TaskRepository taskRepo, WorktreeRepository worktreeRepo,
- ListRepository listRepo, GitService git, WorkerClient worker)
+ ListRepository listRepo, GitService git, WorkerClient worker, TagRepository tagRepo)
{
_taskRepo = taskRepo;
_worktreeRepo = worktreeRepo;
_listRepo = listRepo;
_git = git;
_worker = worker;
+ _tagRepo = tagRepo;
worker.TaskMessageEvent += OnTaskMessage;
worker.WorktreeUpdatedEvent += OnWorktreeUpdated;
@@ -59,16 +71,77 @@ public partial class TaskDetailViewModel : ViewModelBase
var task = await _taskRepo.GetByIdAsync(taskId);
if (task is null) return;
- _listId = task.ListId;
- Title = task.Title;
- Description = task.Description;
- Result = task.Result;
- LogPath = task.LogPath;
- StatusText = task.Status.ToString().ToLowerInvariant();
+ _isLoading = true;
+ try
+ {
+ _listId = task.ListId;
+ Title = task.Title;
+ Description = task.Description;
+ Result = task.Result;
+ LogPath = task.LogPath;
+ StatusText = task.Status.ToString().ToLowerInvariant();
+ StatusChoice = task.Status.ToString();
+ CommitType = task.CommitType;
+
+ Tags.Clear();
+ var tags = await _taskRepo.GetTagsAsync(taskId);
+ foreach (var tag in tags)
+ Tags.Add(tag);
+ }
+ finally
+ {
+ _isLoading = false;
+ }
await LoadWorktreeAsync(taskId);
}
+ public async Task SaveAsync()
+ {
+ if (_isLoading || _taskId is null) return;
+
+ var entity = await _taskRepo.GetByIdAsync(_taskId);
+ if (entity is null) return;
+
+ entity.Title = Title;
+ entity.Description = Description;
+ entity.CommitType = CommitType;
+
+ if (Enum.TryParse(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;
+
+ 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;
+ await _taskRepo.RemoveTagAsync(_taskId, tag.Id);
+ Tags.Remove(tag);
+ TaskChanged?.Invoke(_taskId);
+ }
+
public void Clear()
{
_taskId = null;
@@ -80,6 +153,10 @@ public partial class TaskDetailViewModel : ViewModelBase
StatusText = "";
HasWorktree = false;
LiveLines.Clear();
+ Tags.Clear();
+ NewTagInput = "";
+ StatusChoice = "Manual";
+ CommitType = "chore";
}
private async Task LoadWorktreeAsync(string taskId)
diff --git a/src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs b/src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs
index bf4c3d7..5dade3c 100644
--- a/src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs
@@ -1,3 +1,4 @@
+using Avalonia.Media;
using ClaudeDo.Data.Models;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -20,9 +21,10 @@ public partial class TaskItemViewModel : ViewModelBase
private readonly Func? _runNow;
private readonly Func _canRunNow;
+ private readonly Func? _toggleDone;
public TaskItemViewModel(TaskEntity entity, IReadOnlyList tags,
- Func? runNow, Func canRunNow)
+ Func? runNow, Func canRunNow, Func? toggleDone = null)
{
Entity = entity;
Id = entity.Id;
@@ -35,8 +37,23 @@ public partial class TaskItemViewModel : ViewModelBase
_description = entity.Description;
_runNow = runNow;
_canRunNow = canRunNow;
+ _toggleDone = toggleDone;
}
+ 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 tags)
{
Entity = entity;
@@ -47,6 +64,13 @@ public partial class TaskItemViewModel : ViewModelBase
CommitType = entity.CommitType;
Description = entity.Description;
RunNowCommand.NotifyCanExecuteChanged();
+ OnPropertyChanged(nameof(IsDone));
+ OnPropertyChanged(nameof(IsRunning));
+ OnPropertyChanged(nameof(CanToggleDone));
+ OnPropertyChanged(nameof(TitleDecorations));
+ OnPropertyChanged(nameof(TitleForeground));
+ OnPropertyChanged(nameof(RowOpacity));
+ ToggleDoneCommand.NotifyCanExecuteChanged();
}
[RelayCommand(CanExecute = nameof(CanRunNow))]
@@ -58,4 +82,11 @@ public partial class TaskItemViewModel : ViewModelBase
private bool CanRunNow() =>
_canRunNow() && Status == TaskStatus.Queued;
+
+ [RelayCommand(CanExecute = nameof(CanToggleDone))]
+ private async Task ToggleDone()
+ {
+ if (_toggleDone is not null)
+ await _toggleDone(Id);
+ }
}
diff --git a/src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs b/src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs
index c81b37a..baf25f7 100644
--- a/src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs
@@ -25,6 +25,8 @@ public partial class TaskListViewModel : ViewModelBase
[ObservableProperty] private TaskItemViewModel? _selectedTask;
[ObservableProperty, NotifyCanExecuteChangedFor(nameof(AddTaskCommand))] private string? _currentListId;
+ [ObservableProperty] private string _listName = "Tasks";
+ [ObservableProperty] private string _inlineAddTitle = "";
public event Action? SelectedTaskChanged;
@@ -61,6 +63,16 @@ public partial class TaskListViewModel : ViewModelBase
Tasks.Clear();
SelectedTask = null;
+ if (listId is not null)
+ {
+ var list = await _listRepo.GetByIdAsync(listId);
+ ListName = list?.Name ?? "Tasks";
+ }
+ else
+ {
+ ListName = "Tasks";
+ }
+
if (listId is null) return;
try
@@ -69,7 +81,7 @@ public partial class TaskListViewModel : ViewModelBase
foreach (var e in entities)
{
var tags = await _taskRepo.GetEffectiveTagsAsync(e.Id);
- Tasks.Add(new TaskItemViewModel(e, tags, RunNowAsync, () => _worker.IsConnected));
+ Tasks.Add(new TaskItemViewModel(e, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync));
}
}
catch (Exception ex)
@@ -80,6 +92,40 @@ public partial class TaskListViewModel : ViewModelBase
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;
+
+ var list = await _listRepo.GetByIdAsync(CurrentListId);
+ var 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
+ {
+ await _taskRepo.AddAsync(entity);
+ var tags = await _taskRepo.GetEffectiveTagsAsync(entity.Id);
+ var vm = new TaskItemViewModel(entity, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync);
+ Tasks.Add(vm);
+ SelectedTask = vm;
+ InlineAddTitle = "";
+ }
+ catch (Exception ex)
+ {
+ _showMessage($"Error creating task: {ex.Message}");
+ }
+ }
+
[RelayCommand(CanExecute = nameof(CanAddTask))]
private async Task AddTask()
{
@@ -108,7 +154,7 @@ public partial class TaskListViewModel : ViewModelBase
}
var tags = await _taskRepo.GetEffectiveTagsAsync(saved.Id);
- Tasks.Add(new TaskItemViewModel(saved, tags, RunNowAsync, () => _worker.IsConnected));
+ Tasks.Add(new TaskItemViewModel(saved, tags, RunNowAsync, () => _worker.IsConnected, ToggleDoneAsync));
// Auto wake-queue if agent+queued
if (saved.Status == TaskStatus.Queued &&
@@ -206,6 +252,19 @@ public partial class TaskListViewModel : ViewModelBase
}
}
+ private async Task ToggleDoneAsync(string taskId)
+ {
+ 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;
diff --git a/src/ClaudeDo.Ui/Views/MainWindow.axaml b/src/ClaudeDo.Ui/Views/MainWindow.axaml
index 0fe8f56..fef54ce 100644
--- a/src/ClaudeDo.Ui/Views/MainWindow.axaml
+++ b/src/ClaudeDo.Ui/Views/MainWindow.axaml
@@ -7,60 +7,97 @@
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="700"
x:Class="ClaudeDo.Ui.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
- Title="ClaudeDo" Width="1200" Height="700"
- MinWidth="800" MinHeight="500">
+ Title="ClaudeDo"
+ MinWidth="800" MinHeight="500"
+ KeyDown="OnGlobalKeyDown">
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
+
+
+
-
-
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ClaudeDo.Ui/Views/MainWindow.axaml.cs b/src/ClaudeDo.Ui/Views/MainWindow.axaml.cs
index fcd8c80..df92e56 100644
--- a/src/ClaudeDo.Ui/Views/MainWindow.axaml.cs
+++ b/src/ClaudeDo.Ui/Views/MainWindow.axaml.cs
@@ -1,5 +1,9 @@
+using System;
+using System.Linq;
using Avalonia.Controls;
using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.VisualTree;
using ClaudeDo.Ui.ViewModels;
namespace ClaudeDo.Ui.Views;
@@ -18,24 +22,54 @@ public partial class MainWindow : Window
await vm.InitializeAsync();
}
- private void OnListItemDoubleTapped(object? sender, TappedEventArgs e)
+ private void OnGlobalKeyDown(object? sender, KeyEventArgs e)
{
- if (sender is Control c && c.DataContext is ListItemViewModel
- && DataContext is MainWindowViewModel vm)
+ if (DataContext is not MainWindowViewModel vm) return;
+
+ var ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control);
+ var shift = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
+
+ if (ctrl && shift && e.Key == Key.N)
{
- vm.EditListCommand.Execute(null);
+ vm.AddListCommand.Execute(null);
e.Handled = true;
}
+ else if (ctrl && e.Key == Key.N)
+ {
+ var taskListView = this.GetVisualDescendants().OfType().FirstOrDefault();
+ taskListView?.FocusInlineAdd();
+ e.Handled = true;
+ }
+ else if (ctrl && e.Key == Key.L)
+ {
+ this.FindControl("ListsBox")?.Focus();
+ e.Handled = true;
+ }
+ else if (ctrl && e.Key == Key.R)
+ {
+ if (vm.TaskList.SelectedTask is { } task)
+ {
+ task.RunNowCommand.Execute(null);
+ e.Handled = true;
+ }
+ }
+ }
+
+ private void OnListItemDoubleTapped(object? sender, TappedEventArgs e)
+ {
+ if (DataContext is MainWindowViewModel vm)
+ vm.EditListCommand.Execute(null);
}
private void OnListItemPointerPressed(object? sender, PointerPressedEventArgs e)
{
- if (e.GetCurrentPoint(null).Properties.PointerUpdateKind == PointerUpdateKind.RightButtonPressed
- && sender is Control c && c.DataContext is ListItemViewModel item
+ var props = e.GetCurrentPoint(this).Properties;
+ if (!props.IsRightButtonPressed) return;
+
+ if (sender is Grid { DataContext: ListItemViewModel item }
&& DataContext is MainWindowViewModel vm)
{
vm.SelectedList = item;
- e.Handled = true;
}
}
}
diff --git a/src/ClaudeDo.Ui/Views/TaskDetailView.axaml b/src/ClaudeDo.Ui/Views/TaskDetailView.axaml
index e55eb41..2f605a9 100644
--- a/src/ClaudeDo.Ui/Views/TaskDetailView.axaml
+++ b/src/ClaudeDo.Ui/Views/TaskDetailView.axaml
@@ -2,80 +2,148 @@
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">
-
-
-
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FontSize="11" TextWrapping="NoWrap"
+ Foreground="{StaticResource TextPrimaryBrush}"/>
-
-
+
-
+
-
-
+
+
-
-
+
+
-
-
diff --git a/src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs b/src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs
index 61ef850..70546b3 100644
--- a/src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs
+++ b/src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs
@@ -1,4 +1,7 @@
using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using ClaudeDo.Ui.ViewModels;
namespace ClaudeDo.Ui.Views;
@@ -8,4 +11,24 @@ public partial class TaskDetailView : UserControl
{
InitializeComponent();
}
+
+ private async void OnFieldLostFocus(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is TaskDetailViewModel vm)
+ 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("TitleBox")?.Focus();
+ }
}
diff --git a/src/ClaudeDo.Ui/Views/TaskListView.axaml b/src/ClaudeDo.Ui/Views/TaskListView.axaml
index 2d75156..063bc06 100644
--- a/src/ClaudeDo.Ui/Views/TaskListView.axaml
+++ b/src/ClaudeDo.Ui/Views/TaskListView.axaml
@@ -6,20 +6,36 @@
x:DataType="vm:TaskListViewModel"
x:Name="Root">
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ Background="Transparent"
+ Margin="4,0"
+ KeyDown="OnTaskListKeyDown">
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ClaudeDo.Ui/Views/TaskListView.axaml.cs b/src/ClaudeDo.Ui/Views/TaskListView.axaml.cs
index 23c01e0..c739070 100644
--- a/src/ClaudeDo.Ui/Views/TaskListView.axaml.cs
+++ b/src/ClaudeDo.Ui/Views/TaskListView.axaml.cs
@@ -1,5 +1,7 @@
using Avalonia.Controls;
using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.VisualTree;
using ClaudeDo.Ui.ViewModels;
namespace ClaudeDo.Ui.Views;
@@ -11,24 +13,92 @@ public partial class TaskListView : UserControl
InitializeComponent();
}
- private void OnTaskItemDoubleTapped(object? sender, TappedEventArgs e)
+ private void OnInlineAddKeyDown(object? sender, KeyEventArgs e)
{
- if (sender is Control c && c.DataContext is TaskItemViewModel
- && DataContext is TaskListViewModel vm)
+ if (DataContext is not TaskListViewModel vm) return;
+
+ if (e.Key == Key.Enter)
{
- vm.EditTaskCommand.Execute(null);
+ vm.InlineAddCommand.Execute(null);
+ e.Handled = true;
+ }
+ else if (e.Key == Key.Escape)
+ {
+ vm.InlineAddTitle = "";
+ this.FindControl("TaskListBox")?.Focus();
e.Handled = true;
}
}
- private void OnTaskItemPointerPressed(object? sender, PointerPressedEventArgs e)
+ private void OnInlineAddGotFocus(object? sender, FocusChangedEventArgs e)
{
- if (e.GetCurrentPoint(null).Properties.PointerUpdateKind == PointerUpdateKind.RightButtonPressed
- && sender is Control c && c.DataContext is TaskItemViewModel item
- && DataContext is TaskListViewModel vm)
+ 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)
{
- vm.SelectedTask = item;
+ 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().FirstOrDefault()
+ ?.GetVisualDescendants().OfType().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;
+ }
+ }
+
+ public void FocusInlineAdd()
+ {
+ this.FindControl("InlineAddBox")?.Focus();
+ }
}