feat/ui-improvements #1
@@ -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 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)
|
||||
INSERT OR IGNORE INTO tags (name) VALUES ('agent');
|
||||
INSERT OR IGNORE INTO tags (name) VALUES ('manual');
|
||||
|
||||
@@ -49,6 +49,7 @@ sealed class Program
|
||||
// Repositories
|
||||
sc.AddSingleton<ListRepository>();
|
||||
sc.AddSingleton<TaskRepository>();
|
||||
sc.AddSingleton<SubtaskRepository>();
|
||||
sc.AddSingleton<TagRepository>();
|
||||
sc.AddSingleton<WorktreeRepository>();
|
||||
|
||||
@@ -66,7 +67,8 @@ sealed class Program
|
||||
sp.GetRequiredService<ListRepository>(),
|
||||
sp.GetRequiredService<GitService>(),
|
||||
sp.GetRequiredService<WorkerClient>(),
|
||||
sp.GetRequiredService<TagRepository>()));
|
||||
sp.GetRequiredService<TagRepository>(),
|
||||
sp.GetRequiredService<SubtaskRepository>()));
|
||||
sc.AddSingleton<TaskListViewModel>(sp =>
|
||||
{
|
||||
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)),
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,7 @@ builder.Services.AddSingleton(dbFactory);
|
||||
builder.Services.AddSingleton<TagRepository>();
|
||||
builder.Services.AddSingleton<ListRepository>();
|
||||
builder.Services.AddSingleton<TaskRepository>();
|
||||
builder.Services.AddSingleton<SubtaskRepository>();
|
||||
builder.Services.AddSingleton<WorktreeRepository>();
|
||||
builder.Services.AddSingleton<TaskRunRepository>();
|
||||
builder.Services.AddHostedService<StaleTaskRecovery>();
|
||||
|
||||
@@ -12,6 +12,7 @@ public sealed class TaskRunner
|
||||
private readonly TaskRunRepository _runRepo;
|
||||
private readonly ListRepository _listRepo;
|
||||
private readonly WorktreeRepository _wtRepo;
|
||||
private readonly SubtaskRepository _subtaskRepo;
|
||||
private readonly HubBroadcaster _broadcaster;
|
||||
private readonly WorktreeManager _wtManager;
|
||||
private readonly ClaudeArgsBuilder _argsBuilder;
|
||||
@@ -24,6 +25,7 @@ public sealed class TaskRunner
|
||||
TaskRunRepository runRepo,
|
||||
ListRepository listRepo,
|
||||
WorktreeRepository wtRepo,
|
||||
SubtaskRepository subtaskRepo,
|
||||
HubBroadcaster broadcaster,
|
||||
WorktreeManager wtManager,
|
||||
ClaudeArgsBuilder argsBuilder,
|
||||
@@ -35,6 +37,7 @@ public sealed class TaskRunner
|
||||
_runRepo = runRepo;
|
||||
_listRepo = listRepo;
|
||||
_wtRepo = wtRepo;
|
||||
_subtaskRepo = subtaskRepo;
|
||||
_broadcaster = broadcaster;
|
||||
_wtManager = wtManager;
|
||||
_argsBuilder = argsBuilder;
|
||||
@@ -91,9 +94,16 @@ public sealed class TaskRunner
|
||||
await _broadcaster.TaskStarted(slot, task.Id, now);
|
||||
|
||||
// Build prompt.
|
||||
var prompt = string.IsNullOrWhiteSpace(task.Description)
|
||||
? task.Title
|
||||
: $"{task.Title}\n\n{task.Description.Trim()}";
|
||||
var subtasks = await _subtaskRepo.GetByTaskIdAsync(task.Id, ct);
|
||||
var sb = new System.Text.StringBuilder(task.Title);
|
||||
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.
|
||||
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 wtManager = new WorktreeManager(new GitService(), wtRepo, _cfg, NullLogger<WorktreeManager>.Instance);
|
||||
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);
|
||||
var service = new QueueService(_taskRepo, runner, _cfg, NullLogger<QueueService>.Instance);
|
||||
return (service, fake);
|
||||
|
||||
Reference in New Issue
Block a user