Compare commits
5 Commits
6afe5959ca
...
c9e38aef88
| Author | SHA1 | Date | |
|---|---|---|---|
| c9e38aef88 | |||
| 66843d242b | |||
|
|
9a407bde83 | ||
|
|
8c051d8f62 | ||
|
|
8577c55685 |
@@ -85,6 +85,16 @@ CREATE TABLE IF NOT EXISTS task_runs (
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_task_runs_task_id ON task_runs(task_id);
|
CREATE INDEX IF NOT EXISTS idx_task_runs_task_id ON task_runs(task_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS subtasks (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
completed INTEGER NOT NULL DEFAULT 0,
|
||||||
|
order_num INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_subtasks_task_id ON subtasks(task_id);
|
||||||
|
|
||||||
-- Seed: minimal tag set (ignored if already present)
|
-- Seed: minimal tag set (ignored if already present)
|
||||||
INSERT OR IGNORE INTO tags (name) VALUES ('agent');
|
INSERT OR IGNORE INTO tags (name) VALUES ('agent');
|
||||||
INSERT OR IGNORE INTO tags (name) VALUES ('manual');
|
INSERT OR IGNORE INTO tags (name) VALUES ('manual');
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ sealed class Program
|
|||||||
// Repositories
|
// Repositories
|
||||||
sc.AddSingleton<ListRepository>();
|
sc.AddSingleton<ListRepository>();
|
||||||
sc.AddSingleton<TaskRepository>();
|
sc.AddSingleton<TaskRepository>();
|
||||||
|
sc.AddSingleton<SubtaskRepository>();
|
||||||
sc.AddSingleton<TagRepository>();
|
sc.AddSingleton<TagRepository>();
|
||||||
sc.AddSingleton<WorktreeRepository>();
|
sc.AddSingleton<WorktreeRepository>();
|
||||||
|
|
||||||
@@ -66,7 +67,8 @@ sealed class Program
|
|||||||
sp.GetRequiredService<ListRepository>(),
|
sp.GetRequiredService<ListRepository>(),
|
||||||
sp.GetRequiredService<GitService>(),
|
sp.GetRequiredService<GitService>(),
|
||||||
sp.GetRequiredService<WorkerClient>(),
|
sp.GetRequiredService<WorkerClient>(),
|
||||||
sp.GetRequiredService<TagRepository>()));
|
sp.GetRequiredService<TagRepository>(),
|
||||||
|
sp.GetRequiredService<SubtaskRepository>()));
|
||||||
sc.AddSingleton<TaskListViewModel>(sp =>
|
sc.AddSingleton<TaskListViewModel>(sp =>
|
||||||
{
|
{
|
||||||
var taskRepo = sp.GetRequiredService<TaskRepository>();
|
var taskRepo = sp.GetRequiredService<TaskRepository>();
|
||||||
|
|||||||
11
src/ClaudeDo.Data/Models/SubtaskEntity.cs
Normal file
11
src/ClaudeDo.Data/Models/SubtaskEntity.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace ClaudeDo.Data.Models;
|
||||||
|
|
||||||
|
public sealed class SubtaskEntity
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string TaskId { get; init; }
|
||||||
|
public required string Title { get; set; }
|
||||||
|
public bool Completed { get; set; }
|
||||||
|
public int OrderNum { get; set; }
|
||||||
|
public required DateTime CreatedAt { get; init; }
|
||||||
|
}
|
||||||
81
src/ClaudeDo.Data/Repositories/SubtaskRepository.cs
Normal file
81
src/ClaudeDo.Data/Repositories/SubtaskRepository.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Repositories;
|
||||||
|
|
||||||
|
public sealed class SubtaskRepository
|
||||||
|
{
|
||||||
|
private readonly SqliteConnectionFactory _factory;
|
||||||
|
|
||||||
|
public SubtaskRepository(SqliteConnectionFactory factory) => _factory = factory;
|
||||||
|
|
||||||
|
public async Task<List<SubtaskEntity>> GetByTaskIdAsync(string taskId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await using var conn = _factory.Open();
|
||||||
|
await using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT id, task_id, title, completed, order_num, created_at FROM subtasks WHERE task_id = @task_id ORDER BY order_num";
|
||||||
|
cmd.Parameters.AddWithValue("@task_id", taskId);
|
||||||
|
|
||||||
|
await using var reader = await cmd.ExecuteReaderAsync(ct);
|
||||||
|
var result = new List<SubtaskEntity>();
|
||||||
|
while (await reader.ReadAsync(ct))
|
||||||
|
result.Add(ReadSubtask(reader));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddAsync(SubtaskEntity entity, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await using var conn = _factory.Open();
|
||||||
|
await using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = """
|
||||||
|
INSERT INTO subtasks (id, task_id, title, completed, order_num, created_at)
|
||||||
|
VALUES (@id, @task_id, @title, @completed, @order_num, @created_at)
|
||||||
|
""";
|
||||||
|
BindSubtask(cmd, entity);
|
||||||
|
await cmd.ExecuteNonQueryAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(SubtaskEntity entity, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await using var conn = _factory.Open();
|
||||||
|
await using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = """
|
||||||
|
UPDATE subtasks SET title = @title, completed = @completed, order_num = @order_num
|
||||||
|
WHERE id = @id
|
||||||
|
""";
|
||||||
|
cmd.Parameters.AddWithValue("@id", entity.Id);
|
||||||
|
cmd.Parameters.AddWithValue("@title", entity.Title);
|
||||||
|
cmd.Parameters.AddWithValue("@completed", entity.Completed ? 1 : 0);
|
||||||
|
cmd.Parameters.AddWithValue("@order_num", entity.OrderNum);
|
||||||
|
await cmd.ExecuteNonQueryAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(string id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await using var conn = _factory.Open();
|
||||||
|
await using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = "DELETE FROM subtasks WHERE id = @id";
|
||||||
|
cmd.Parameters.AddWithValue("@id", id);
|
||||||
|
await cmd.ExecuteNonQueryAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BindSubtask(SqliteCommand cmd, SubtaskEntity e)
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@id", e.Id);
|
||||||
|
cmd.Parameters.AddWithValue("@task_id", e.TaskId);
|
||||||
|
cmd.Parameters.AddWithValue("@title", e.Title);
|
||||||
|
cmd.Parameters.AddWithValue("@completed", e.Completed ? 1 : 0);
|
||||||
|
cmd.Parameters.AddWithValue("@order_num", e.OrderNum);
|
||||||
|
cmd.Parameters.AddWithValue("@created_at", e.CreatedAt.ToString("o"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SubtaskEntity ReadSubtask(SqliteDataReader r) => new()
|
||||||
|
{
|
||||||
|
Id = r.GetString(0),
|
||||||
|
TaskId = r.GetString(1),
|
||||||
|
Title = r.GetString(2),
|
||||||
|
Completed = r.GetInt64(3) != 0,
|
||||||
|
OrderNum = r.GetInt32(4),
|
||||||
|
CreatedAt = DateTime.Parse(r.GetString(5)),
|
||||||
|
};
|
||||||
|
}
|
||||||
23
src/ClaudeDo.Ui/ViewModels/SubtaskItemViewModel.cs
Normal file
23
src/ClaudeDo.Ui/ViewModels/SubtaskItemViewModel.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ public partial class TaskDetailViewModel : ViewModelBase
|
|||||||
private readonly GitService _git;
|
private readonly GitService _git;
|
||||||
private readonly WorkerClient _worker;
|
private readonly WorkerClient _worker;
|
||||||
private readonly TagRepository _tagRepo;
|
private readonly TagRepository _tagRepo;
|
||||||
|
private readonly SubtaskRepository _subtaskRepo;
|
||||||
|
|
||||||
[ObservableProperty] private string _title = "";
|
[ObservableProperty] private string _title = "";
|
||||||
[ObservableProperty] private string? _description;
|
[ObservableProperty] private string? _description;
|
||||||
@@ -28,9 +29,14 @@ public partial class TaskDetailViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private string _statusText = "";
|
[ObservableProperty] private string _statusText = "";
|
||||||
[ObservableProperty] private string _statusChoice = "Manual";
|
[ObservableProperty] private string _statusChoice = "Manual";
|
||||||
[ObservableProperty] private string _commitType = "chore";
|
[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[] 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[] CommitTypes { get; } = ["feat", "fix", "refactor", "docs", "test", "chore", "ci", "perf", "style", "build"];
|
||||||
|
public static string[] ModelChoices { get; } = ["(list default)", "Sonnet", "Opus", "Haiku"];
|
||||||
|
|
||||||
// Worktree
|
// Worktree
|
||||||
[ObservableProperty] private bool _hasWorktree;
|
[ObservableProperty] private bool _hasWorktree;
|
||||||
@@ -44,6 +50,7 @@ public partial class TaskDetailViewModel : ViewModelBase
|
|||||||
private StreamLineFormatter _formatter = new();
|
private StreamLineFormatter _formatter = new();
|
||||||
public ObservableCollection<TagEntity> Tags { get; } = new();
|
public ObservableCollection<TagEntity> Tags { get; } = new();
|
||||||
[ObservableProperty] private string _newTagInput = "";
|
[ObservableProperty] private string _newTagInput = "";
|
||||||
|
public ObservableCollection<SubtaskItemViewModel> Subtasks { get; } = new();
|
||||||
|
|
||||||
private string? _taskId;
|
private string? _taskId;
|
||||||
private string? _listId;
|
private string? _listId;
|
||||||
@@ -52,7 +59,8 @@ public partial class TaskDetailViewModel : ViewModelBase
|
|||||||
public event Action<string>? TaskChanged;
|
public event Action<string>? TaskChanged;
|
||||||
|
|
||||||
public TaskDetailViewModel(TaskRepository taskRepo, WorktreeRepository worktreeRepo,
|
public TaskDetailViewModel(TaskRepository taskRepo, WorktreeRepository worktreeRepo,
|
||||||
ListRepository listRepo, GitService git, WorkerClient worker, TagRepository tagRepo)
|
ListRepository listRepo, GitService git, WorkerClient worker, TagRepository tagRepo,
|
||||||
|
SubtaskRepository subtaskRepo)
|
||||||
{
|
{
|
||||||
_taskRepo = taskRepo;
|
_taskRepo = taskRepo;
|
||||||
_worktreeRepo = worktreeRepo;
|
_worktreeRepo = worktreeRepo;
|
||||||
@@ -60,6 +68,7 @@ public partial class TaskDetailViewModel : ViewModelBase
|
|||||||
_git = git;
|
_git = git;
|
||||||
_worker = worker;
|
_worker = worker;
|
||||||
_tagRepo = tagRepo;
|
_tagRepo = tagRepo;
|
||||||
|
_subtaskRepo = subtaskRepo;
|
||||||
|
|
||||||
worker.TaskMessageEvent += OnTaskMessage;
|
worker.TaskMessageEvent += OnTaskMessage;
|
||||||
worker.WorktreeUpdatedEvent += OnWorktreeUpdated;
|
worker.WorktreeUpdatedEvent += OnWorktreeUpdated;
|
||||||
@@ -77,6 +86,13 @@ public partial class TaskDetailViewModel : ViewModelBase
|
|||||||
var task = await _taskRepo.GetByIdAsync(taskId);
|
var task = await _taskRepo.GetByIdAsync(taskId);
|
||||||
if (task is null) return;
|
if (task is null) return;
|
||||||
|
|
||||||
|
if (AvailableAgents.Count == 0)
|
||||||
|
{
|
||||||
|
var agents = await _worker.GetAgentsAsync();
|
||||||
|
AvailableAgents.AddRange(agents);
|
||||||
|
OnPropertyChanged(nameof(AvailableAgents));
|
||||||
|
}
|
||||||
|
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -95,11 +111,39 @@ public partial class TaskDetailViewModel : ViewModelBase
|
|||||||
StatusText = task.Status.ToString().ToLowerInvariant();
|
StatusText = task.Status.ToString().ToLowerInvariant();
|
||||||
StatusChoice = task.Status.ToString();
|
StatusChoice = task.Status.ToString();
|
||||||
CommitType = task.CommitType;
|
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();
|
Tags.Clear();
|
||||||
var tags = await _taskRepo.GetTagsAsync(taskId);
|
var tags = await _taskRepo.GetTagsAsync(taskId);
|
||||||
foreach (var tag in tags)
|
foreach (var tag in tags)
|
||||||
Tags.Add(tag);
|
Tags.Add(tag);
|
||||||
|
|
||||||
|
Subtasks.Clear();
|
||||||
|
var subtasks = await _subtaskRepo.GetByTaskIdAsync(taskId);
|
||||||
|
foreach (var s in subtasks)
|
||||||
|
{
|
||||||
|
var vm = SubtaskItemViewModel.From(s);
|
||||||
|
vm.PropertyChanged += OnSubtaskPropertyChanged;
|
||||||
|
Subtasks.Add(vm);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -119,6 +163,11 @@ public partial class TaskDetailViewModel : ViewModelBase
|
|||||||
entity.Title = Title;
|
entity.Title = Title;
|
||||||
entity.Description = Description;
|
entity.Description = Description;
|
||||||
entity.CommitType = CommitType;
|
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))
|
if (Enum.TryParse<Data.Models.TaskStatus>(StatusChoice, true, out var status))
|
||||||
entity.Status = status;
|
entity.Status = status;
|
||||||
@@ -155,6 +204,61 @@ public partial class TaskDetailViewModel : ViewModelBase
|
|||||||
TaskChanged?.Invoke(_taskId);
|
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,
|
||||||
|
};
|
||||||
|
await _subtaskRepo.AddAsync(entity);
|
||||||
|
var vm = SubtaskItemViewModel.From(entity);
|
||||||
|
vm.PropertyChanged += OnSubtaskPropertyChanged;
|
||||||
|
Subtasks.Add(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task RemoveSubtask(SubtaskItemViewModel item)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(item.Id))
|
||||||
|
await _subtaskRepo.DeleteAsync(item.Id);
|
||||||
|
item.PropertyChanged -= OnSubtaskPropertyChanged;
|
||||||
|
Subtasks.Remove(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
await _subtaskRepo.UpdateAsync(new SubtaskEntity
|
||||||
|
{
|
||||||
|
Id = vm.Id,
|
||||||
|
TaskId = _taskId ?? "",
|
||||||
|
Title = vm.Title,
|
||||||
|
Completed = vm.Completed,
|
||||||
|
OrderNum = Subtasks.IndexOf(vm),
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
public void Clear()
|
||||||
{
|
{
|
||||||
_taskId = null;
|
_taskId = null;
|
||||||
@@ -169,8 +273,13 @@ public partial class TaskDetailViewModel : ViewModelBase
|
|||||||
_formatter = new StreamLineFormatter();
|
_formatter = new StreamLineFormatter();
|
||||||
Tags.Clear();
|
Tags.Clear();
|
||||||
NewTagInput = "";
|
NewTagInput = "";
|
||||||
|
foreach (var s in Subtasks) s.PropertyChanged -= OnSubtaskPropertyChanged;
|
||||||
|
Subtasks.Clear();
|
||||||
StatusChoice = "Manual";
|
StatusChoice = "Manual";
|
||||||
CommitType = "chore";
|
CommitType = "chore";
|
||||||
|
ModelChoice = "(list default)";
|
||||||
|
SystemPromptOverride = null;
|
||||||
|
SelectedAgent = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadWorktreeAsync(string taskId)
|
private async Task LoadWorktreeAsync(string taskId)
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
using ClaudeDo.Data.Models;
|
using ClaudeDo.Data.Models;
|
||||||
|
using ClaudeDo.Data.Repositories;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
@@ -8,6 +11,8 @@ namespace ClaudeDo.Ui.ViewModels;
|
|||||||
|
|
||||||
public partial class TaskEditorViewModel : ViewModelBase
|
public partial class TaskEditorViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
|
private readonly SubtaskRepository _subtaskRepo;
|
||||||
|
|
||||||
[ObservableProperty] private string _title = "";
|
[ObservableProperty] private string _title = "";
|
||||||
[ObservableProperty] private string? _description;
|
[ObservableProperty] private string? _description;
|
||||||
[ObservableProperty] private string _commitType = "chore";
|
[ObservableProperty] private string _commitType = "chore";
|
||||||
@@ -18,6 +23,7 @@ public partial class TaskEditorViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private string? _systemPromptOverride;
|
[ObservableProperty] private string? _systemPromptOverride;
|
||||||
[ObservableProperty] private AgentInfo? _selectedAgent;
|
[ObservableProperty] private AgentInfo? _selectedAgent;
|
||||||
public List<AgentInfo> AvailableAgents { get; set; } = [];
|
public List<AgentInfo> AvailableAgents { get; set; } = [];
|
||||||
|
public ObservableCollection<SubtaskItemViewModel> Subtasks { get; } = new();
|
||||||
|
|
||||||
private string? _editId;
|
private string? _editId;
|
||||||
private string _listId = "";
|
private string _listId = "";
|
||||||
@@ -34,11 +40,28 @@ public partial class TaskEditorViewModel : ViewModelBase
|
|||||||
public static string[] StatusChoices { get; } =
|
public static string[] StatusChoices { get; } =
|
||||||
["manual", "queued"];
|
["manual", "queued"];
|
||||||
|
|
||||||
|
public TaskEditorViewModel(SubtaskRepository subtaskRepo)
|
||||||
|
{
|
||||||
|
_subtaskRepo = subtaskRepo;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task LoadAgentsAsync(WorkerClient worker)
|
public async Task LoadAgentsAsync(WorkerClient worker)
|
||||||
{
|
{
|
||||||
AvailableAgents = await worker.GetAgentsAsync();
|
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 =>
|
public IReadOnlyList<string> SelectedTagNames =>
|
||||||
TagsInput.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
TagsInput.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
@@ -51,8 +74,54 @@ public partial class TaskEditorViewModel : ViewModelBase
|
|||||||
_createdAt = DateTime.UtcNow;
|
_createdAt = DateTime.UtcNow;
|
||||||
CommitType = defaultCommitType;
|
CommitType = defaultCommitType;
|
||||||
WindowTitle = "New Task";
|
WindowTitle = "New Task";
|
||||||
|
Subtasks.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task InitForEditAsync(TaskEntity entity, IReadOnlyList<TagEntity> taskTags, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
_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();
|
||||||
|
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)
|
public void InitForEdit(TaskEntity entity, IReadOnlyList<TagEntity> taskTags)
|
||||||
{
|
{
|
||||||
_editId = entity.Id;
|
_editId = entity.Id;
|
||||||
@@ -72,14 +141,34 @@ public partial class TaskEditorViewModel : ViewModelBase
|
|||||||
? ListEditorViewModel.ModelIdToDisplay(entity.Model)
|
? ListEditorViewModel.ModelIdToDisplay(entity.Model)
|
||||||
: "(list default)";
|
: "(list default)";
|
||||||
SystemPromptOverride = entity.SystemPrompt;
|
SystemPromptOverride = entity.SystemPrompt;
|
||||||
SelectedAgent = entity.AgentPath is not null
|
|
||||||
? AvailableAgents.FirstOrDefault(a => a.Path == entity.AgentPath)
|
if (entity.AgentPath is not null)
|
||||||
: 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}";
|
WindowTitle = $"Edit Task: {entity.Title}";
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void Save()
|
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;
|
if (string.IsNullOrWhiteSpace(Title)) return;
|
||||||
var status = StatusChoice switch
|
var status = StatusChoice switch
|
||||||
@@ -87,9 +176,10 @@ public partial class TaskEditorViewModel : ViewModelBase
|
|||||||
"queued" => TaskStatus.Queued,
|
"queued" => TaskStatus.Queued,
|
||||||
_ => TaskStatus.Manual,
|
_ => TaskStatus.Manual,
|
||||||
};
|
};
|
||||||
|
var taskId = _editId ?? Guid.NewGuid().ToString();
|
||||||
var entity = new TaskEntity
|
var entity = new TaskEntity
|
||||||
{
|
{
|
||||||
Id = _editId ?? Guid.NewGuid().ToString(),
|
Id = taskId,
|
||||||
ListId = _listId,
|
ListId = _listId,
|
||||||
Title = Title.Trim(),
|
Title = Title.Trim(),
|
||||||
Description = string.IsNullOrWhiteSpace(Description) ? null : Description.Trim(),
|
Description = string.IsNullOrWhiteSpace(Description) ? null : Description.Trim(),
|
||||||
@@ -102,6 +192,42 @@ public partial class TaskEditorViewModel : ViewModelBase
|
|||||||
: null;
|
: null;
|
||||||
entity.SystemPrompt = string.IsNullOrWhiteSpace(SystemPromptOverride) ? null : SystemPromptOverride.Trim();
|
entity.SystemPrompt = string.IsNullOrWhiteSpace(SystemPromptOverride) ? null : SystemPromptOverride.Trim();
|
||||||
entity.AgentPath = SelectedAgent?.Path;
|
entity.AgentPath = SelectedAgent?.Path;
|
||||||
|
|
||||||
|
// Persist subtask changes
|
||||||
|
if (_editId is not null)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
await _subtaskRepo.UpdateAsync(new SubtaskEntity { Id = vm.Id, TaskId = taskId, Title = vm.Title, Completed = vm.Completed, OrderNum = idx, 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)
|
||||||
|
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);
|
_tcs.TrySetResult(entity);
|
||||||
RequestClose?.Invoke();
|
RequestClose?.Invoke();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ public partial class TaskListViewModel : ViewModelBase
|
|||||||
var taskTags = await _taskRepo.GetTagsAsync(entity.Id);
|
var taskTags = await _taskRepo.GetTagsAsync(entity.Id);
|
||||||
var editor = _editorFactory();
|
var editor = _editorFactory();
|
||||||
await editor.LoadAgentsAsync(_worker);
|
await editor.LoadAgentsAsync(_worker);
|
||||||
editor.InitForEdit(entity, taskTags);
|
await editor.InitForEditAsync(entity, taskTags);
|
||||||
|
|
||||||
var window = new TaskEditorView { DataContext = editor };
|
var window = new TaskEditorView { DataContext = editor };
|
||||||
editor.RequestClose += () => window.Close();
|
editor.RequestClose += () => window.Close();
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<!-- Lists island -->
|
<!-- Lists island -->
|
||||||
<Border Grid.Column="0" CornerRadius="12" Background="{StaticResource IslandBgBrush}"
|
<Border Grid.Column="0" CornerRadius="12" Background="{StaticResource IslandBgBrush}"
|
||||||
MinWidth="180" MaxWidth="320" Margin="0,0,4,8" ClipToBounds="True">
|
MinWidth="180" Margin="0,0,4,8" ClipToBounds="True">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<TextBlock DockPanel.Dock="Top"
|
<TextBlock DockPanel.Dock="Top"
|
||||||
Text="Lists" FontWeight="SemiBold" FontSize="13"
|
Text="Lists" FontWeight="SemiBold" FontSize="13"
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
|
|
||||||
<!-- Detail island -->
|
<!-- Detail island -->
|
||||||
<Border Grid.Column="2" CornerRadius="12" Background="{StaticResource IslandBgBrush}"
|
<Border Grid.Column="2" CornerRadius="12" Background="{StaticResource IslandBgBrush}"
|
||||||
MinWidth="280" MaxWidth="500" Margin="4,0,0,8" ClipToBounds="True">
|
MinWidth="280" Margin="4,0,0,8" ClipToBounds="True">
|
||||||
<v:TaskDetailView DataContext="{Binding TaskDetail}" />
|
<v:TaskDetailView DataContext="{Binding TaskDetail}" />
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -86,6 +86,71 @@
|
|||||||
PlaceholderText="Add a description..."
|
PlaceholderText="Add a description..."
|
||||||
LostFocus="OnFieldLostFocus"/>
|
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 === -->
|
<!-- === READ-ONLY ZONE === -->
|
||||||
|
|
||||||
<TextBlock Text="Result" FontWeight="SemiBold" FontSize="12"
|
<TextBlock Text="Result" FontWeight="SemiBold" FontSize="12"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.ComponentModel;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
using ClaudeDo.Ui.ViewModels;
|
using ClaudeDo.Ui.ViewModels;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views;
|
namespace ClaudeDo.Ui.Views;
|
||||||
@@ -19,6 +20,31 @@ public partial class TaskDetailView : UserControl
|
|||||||
await vm.SaveAsync();
|
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)
|
private void OnTagInputKeyDown(object? sender, KeyEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Key == Key.Enter && DataContext is TaskDetailViewModel vm)
|
if (e.Key == Key.Enter && DataContext is TaskDetailViewModel vm)
|
||||||
|
|||||||
@@ -35,6 +35,30 @@
|
|||||||
<TextBlock Text="Tags (comma-separated)" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
|
<TextBlock Text="Tags (comma-separated)" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
<TextBox Text="{Binding TagsInput}" PlaceholderText="agent, manual, code, ..."/>
|
<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 -->
|
<!-- Divider -->
|
||||||
<Border Height="1" Background="{StaticResource BorderSubtleBrush}" Margin="0,6,0,2"/>
|
<Border Height="1" Background="{StaticResource BorderSubtleBrush}" Margin="0,6,0,2"/>
|
||||||
|
|
||||||
@@ -55,15 +79,18 @@
|
|||||||
|
|
||||||
<TextBlock Text="Agent File" FontWeight="SemiBold"
|
<TextBlock Text="Agent File" FontWeight="SemiBold"
|
||||||
Foreground="{StaticResource TextSecondaryBrush}"/>
|
Foreground="{StaticResource TextSecondaryBrush}"/>
|
||||||
<ComboBox ItemsSource="{Binding AvailableAgents}"
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
SelectedItem="{Binding SelectedAgent}"
|
<ComboBox ItemsSource="{Binding AvailableAgents}"
|
||||||
MinWidth="150">
|
SelectedItem="{Binding SelectedAgent}"
|
||||||
<ComboBox.ItemTemplate>
|
MinWidth="150">
|
||||||
<DataTemplate x:DataType="models:AgentInfo">
|
<ComboBox.ItemTemplate>
|
||||||
<TextBlock Text="{Binding Name}"/>
|
<DataTemplate x:DataType="models:AgentInfo">
|
||||||
</DataTemplate>
|
<TextBlock Text="{Binding Name}"/>
|
||||||
</ComboBox.ItemTemplate>
|
</DataTemplate>
|
||||||
</ComboBox>
|
</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">
|
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right" Margin="0,10,0,0">
|
||||||
<Button Content="Save" Command="{Binding SaveCommand}" IsDefault="True" MinWidth="80"
|
<Button Content="Save" Command="{Binding SaveCommand}" IsDefault="True" MinWidth="80"
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
using ClaudeDo.Ui.ViewModels;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views;
|
namespace ClaudeDo.Ui.Views;
|
||||||
|
|
||||||
@@ -8,4 +11,19 @@ public partial class TaskEditorView : Window
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ builder.Services.AddSingleton(dbFactory);
|
|||||||
builder.Services.AddSingleton<TagRepository>();
|
builder.Services.AddSingleton<TagRepository>();
|
||||||
builder.Services.AddSingleton<ListRepository>();
|
builder.Services.AddSingleton<ListRepository>();
|
||||||
builder.Services.AddSingleton<TaskRepository>();
|
builder.Services.AddSingleton<TaskRepository>();
|
||||||
|
builder.Services.AddSingleton<SubtaskRepository>();
|
||||||
builder.Services.AddSingleton<WorktreeRepository>();
|
builder.Services.AddSingleton<WorktreeRepository>();
|
||||||
builder.Services.AddSingleton<TaskRunRepository>();
|
builder.Services.AddSingleton<TaskRunRepository>();
|
||||||
builder.Services.AddHostedService<StaleTaskRecovery>();
|
builder.Services.AddHostedService<StaleTaskRecovery>();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public sealed class TaskRunner
|
|||||||
private readonly TaskRunRepository _runRepo;
|
private readonly TaskRunRepository _runRepo;
|
||||||
private readonly ListRepository _listRepo;
|
private readonly ListRepository _listRepo;
|
||||||
private readonly WorktreeRepository _wtRepo;
|
private readonly WorktreeRepository _wtRepo;
|
||||||
|
private readonly SubtaskRepository _subtaskRepo;
|
||||||
private readonly HubBroadcaster _broadcaster;
|
private readonly HubBroadcaster _broadcaster;
|
||||||
private readonly WorktreeManager _wtManager;
|
private readonly WorktreeManager _wtManager;
|
||||||
private readonly ClaudeArgsBuilder _argsBuilder;
|
private readonly ClaudeArgsBuilder _argsBuilder;
|
||||||
@@ -24,6 +25,7 @@ public sealed class TaskRunner
|
|||||||
TaskRunRepository runRepo,
|
TaskRunRepository runRepo,
|
||||||
ListRepository listRepo,
|
ListRepository listRepo,
|
||||||
WorktreeRepository wtRepo,
|
WorktreeRepository wtRepo,
|
||||||
|
SubtaskRepository subtaskRepo,
|
||||||
HubBroadcaster broadcaster,
|
HubBroadcaster broadcaster,
|
||||||
WorktreeManager wtManager,
|
WorktreeManager wtManager,
|
||||||
ClaudeArgsBuilder argsBuilder,
|
ClaudeArgsBuilder argsBuilder,
|
||||||
@@ -35,6 +37,7 @@ public sealed class TaskRunner
|
|||||||
_runRepo = runRepo;
|
_runRepo = runRepo;
|
||||||
_listRepo = listRepo;
|
_listRepo = listRepo;
|
||||||
_wtRepo = wtRepo;
|
_wtRepo = wtRepo;
|
||||||
|
_subtaskRepo = subtaskRepo;
|
||||||
_broadcaster = broadcaster;
|
_broadcaster = broadcaster;
|
||||||
_wtManager = wtManager;
|
_wtManager = wtManager;
|
||||||
_argsBuilder = argsBuilder;
|
_argsBuilder = argsBuilder;
|
||||||
@@ -91,9 +94,16 @@ public sealed class TaskRunner
|
|||||||
await _broadcaster.TaskStarted(slot, task.Id, now);
|
await _broadcaster.TaskStarted(slot, task.Id, now);
|
||||||
|
|
||||||
// Build prompt.
|
// Build prompt.
|
||||||
var prompt = string.IsNullOrWhiteSpace(task.Description)
|
var subtasks = await _subtaskRepo.GetByTaskIdAsync(task.Id, ct);
|
||||||
? task.Title
|
var sb = new System.Text.StringBuilder(task.Title);
|
||||||
: $"{task.Title}\n\n{task.Description.Trim()}";
|
if (!string.IsNullOrWhiteSpace(task.Description)) sb.Append("\n\n").Append(task.Description.Trim());
|
||||||
|
if (subtasks.Count > 0)
|
||||||
|
{
|
||||||
|
sb.Append("\n\n## Sub-Tasks\n");
|
||||||
|
foreach (var s in subtasks)
|
||||||
|
sb.Append(s.Completed ? "- [x] " : "- [ ] ").Append(s.Title).Append('\n');
|
||||||
|
}
|
||||||
|
var prompt = sb.ToString();
|
||||||
|
|
||||||
// Run 1.
|
// Run 1.
|
||||||
var result = await RunOnceAsync(task.Id, slot, runDir, resolvedConfig, 1, false, prompt, ct);
|
var result = await RunOnceAsync(task.Id, slot, runDir, resolvedConfig, 1, false, prompt, ct);
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ public sealed class QueueServiceTests : IDisposable
|
|||||||
var runRepo = new TaskRunRepository(_db.Factory);
|
var runRepo = new TaskRunRepository(_db.Factory);
|
||||||
var wtManager = new WorktreeManager(new GitService(), wtRepo, _cfg, NullLogger<WorktreeManager>.Instance);
|
var wtManager = new WorktreeManager(new GitService(), wtRepo, _cfg, NullLogger<WorktreeManager>.Instance);
|
||||||
var argsBuilder = new ClaudeArgsBuilder();
|
var argsBuilder = new ClaudeArgsBuilder();
|
||||||
var runner = new TaskRunner(fake, _taskRepo, runRepo, _listRepo, wtRepo, broadcaster, wtManager, argsBuilder, _cfg,
|
var subtaskRepo = new SubtaskRepository(_db.Factory);
|
||||||
|
var runner = new TaskRunner(fake, _taskRepo, runRepo, _listRepo, wtRepo, subtaskRepo, broadcaster, wtManager, argsBuilder, _cfg,
|
||||||
NullLogger<TaskRunner>.Instance);
|
NullLogger<TaskRunner>.Instance);
|
||||||
var service = new QueueService(_taskRepo, runner, _cfg, NullLogger<QueueService>.Instance);
|
var service = new QueueService(_taskRepo, runner, _cfg, NullLogger<QueueService>.Instance);
|
||||||
return (service, fake);
|
return (service, fake);
|
||||||
|
|||||||
Reference in New Issue
Block a user