feat(attachments): inject reference files into the run + clean up files on delete

TaskRunner appends attached files (absolute paths) to the run prompt as the
read-only Reference files section. Task and list deletes now remove the
on-disk attachment dir eagerly, and a startup AttachmentOrphanRecovery sweep
drops any attachments/<taskId>/ whose task no longer exists (covers list
cascade and planning-discard paths).
This commit is contained in:
Mika Kuns
2026-06-26 16:11:48 +02:00
parent 5be4b5c5fb
commit 6a0c0f59a5
15 changed files with 265 additions and 12 deletions
+12
View File
@@ -9,6 +9,18 @@ public sealed class AttachmentStore
public AttachmentStore(string? root = null)
=> _root = root ?? Paths.Expand("~/.todo-app/attachments");
public string Root => _root;
public IReadOnlyList<string> EnumerateTaskIds()
{
if (!Directory.Exists(_root)) return Array.Empty<string>();
return Directory.GetDirectories(_root)
.Select(Path.GetFileName)
.Where(n => n is not null)
.Select(n => n!)
.ToList();
}
public string TaskDir(string taskId)
=> Path.Combine(_root, taskId);
@@ -6,8 +6,13 @@ namespace ClaudeDo.Data.Repositories;
public sealed class ListRepository
{
private readonly ClaudeDoDbContext _context;
private readonly AttachmentStore _attachments;
public ListRepository(ClaudeDoDbContext context) => _context = context;
public ListRepository(ClaudeDoDbContext context, AttachmentStore? attachments = null)
{
_context = context;
_attachments = attachments ?? new AttachmentStore();
}
public async Task AddAsync(ListEntity entity, CancellationToken ct = default)
{
@@ -23,7 +28,13 @@ public sealed class ListRepository
public async Task DeleteAsync(string listId, CancellationToken ct = default)
{
var taskIds = await _context.Tasks
.Where(t => t.ListId == listId)
.Select(t => t.Id)
.ToListAsync(ct);
await _context.Lists.Where(l => l.Id == listId).ExecuteDeleteAsync(ct);
foreach (var id in taskIds)
_attachments.DeleteTaskDir(id);
}
public async Task<ListEntity?> GetByIdAsync(string listId, CancellationToken ct = default)
@@ -7,8 +7,13 @@ namespace ClaudeDo.Data.Repositories;
public sealed class TaskRepository
{
private readonly ClaudeDoDbContext _context;
private readonly AttachmentStore _attachments;
public TaskRepository(ClaudeDoDbContext context) => _context = context;
public TaskRepository(ClaudeDoDbContext context, AttachmentStore? attachments = null)
{
_context = context;
_attachments = attachments ?? new AttachmentStore();
}
#region CRUD
@@ -37,6 +42,7 @@ public sealed class TaskRepository
public async Task DeleteAsync(string taskId, CancellationToken ct = default)
{
await _context.Tasks.Where(t => t.Id == taskId).ExecuteDeleteAsync(ct);
_attachments.DeleteTaskDir(taskId);
}
public async Task<TaskEntity?> GetByIdAsync(string taskId, CancellationToken ct = default)