A list working_dir stored as "C:\Dev\Tests\StaplerTracking\" broke every consumer that puts it on a Windows command line: argv rules read \" as an escaped quote, so the token never closes. "Open in terminal" passed wt.exe a starting directory of C:\Dev\Tests\StaplerTracking" and it failed with 0x8007010b; the same data had already corrupted the ConPTY list handler's arg list in August. Paths.TrimTrailingSeparator is now the single helper (replacing the copies in InteractiveLaunchSpecService and ClaudeHelpLauncher) and ListRepository applies it on Add/Update, which covers every writer: UI create, repo import, hub UpdateList, and MCP CreateList/UpdateList. OpenInTerminal also switches to ArgumentList so its quoting is correct regardless of what's stored.
104 lines
3.8 KiB
C#
104 lines
3.8 KiB
C#
using ClaudeDo.Data.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ClaudeDo.Data.Repositories;
|
|
|
|
public sealed class ListRepository
|
|
{
|
|
private readonly ClaudeDoDbContext _context;
|
|
private readonly AttachmentStore _attachments;
|
|
|
|
public ListRepository(ClaudeDoDbContext context, AttachmentStore? attachments = null)
|
|
{
|
|
_context = context;
|
|
_attachments = attachments ?? new AttachmentStore();
|
|
}
|
|
|
|
public async Task AddAsync(ListEntity entity, CancellationToken ct = default)
|
|
{
|
|
NormalizeWorkingDir(entity);
|
|
_context.Lists.Add(entity);
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
|
|
public async Task UpdateAsync(ListEntity entity, CancellationToken ct = default)
|
|
{
|
|
NormalizeWorkingDir(entity);
|
|
_context.Lists.Update(entity);
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
|
|
// A working dir is user-supplied (typed, folder-picked, imported, or set over MCP) and every
|
|
// consumer eventually hands it to the claude CLI or wt.exe as one quoted command-line token —
|
|
// where a trailing '\' escapes its own closing quote. Normalizing here covers all writers;
|
|
// see Paths.TrimTrailingSeparator.
|
|
private static void NormalizeWorkingDir(ListEntity entity) =>
|
|
entity.WorkingDir = Paths.TrimTrailingSeparator(entity.WorkingDir);
|
|
|
|
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)
|
|
{
|
|
return await _context.Lists.FirstOrDefaultAsync(l => l.Id == listId, ct);
|
|
}
|
|
|
|
public async Task<List<ListEntity>> GetAllAsync(CancellationToken ct = default)
|
|
{
|
|
return await _context.Lists.OrderBy(l => l.SortOrder).ThenBy(l => l.CreatedAt).ToListAsync(ct);
|
|
}
|
|
|
|
public async Task ReorderAsync(IReadOnlyList<string> orderedListIds, CancellationToken ct = default)
|
|
{
|
|
var idSet = orderedListIds.ToHashSet();
|
|
var entities = await _context.Lists.Where(l => idSet.Contains(l.Id)).ToListAsync(ct);
|
|
for (int i = 0; i < orderedListIds.Count; i++)
|
|
{
|
|
var e = entities.FirstOrDefault(x => x.Id == orderedListIds[i]);
|
|
if (e is not null) e.SortOrder = i;
|
|
}
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
|
|
public async Task<ListConfigEntity?> GetConfigAsync(string listId, CancellationToken ct = default)
|
|
{
|
|
return await _context.ListConfigs.FirstOrDefaultAsync(c => c.ListId == listId, ct);
|
|
}
|
|
|
|
public async Task SetConfigAsync(ListConfigEntity config, CancellationToken ct = default)
|
|
{
|
|
var existing = await _context.ListConfigs.FirstOrDefaultAsync(c => c.ListId == config.ListId, ct);
|
|
if (existing is null)
|
|
{
|
|
_context.ListConfigs.Add(config);
|
|
}
|
|
else
|
|
{
|
|
existing.Model = config.Model;
|
|
existing.SystemPrompt = config.SystemPrompt;
|
|
existing.AgentPath = config.AgentPath;
|
|
existing.MaxTurns = config.MaxTurns;
|
|
existing.SessionSkills = config.SessionSkills;
|
|
existing.VerifyCommand = config.VerifyCommand;
|
|
existing.SerializeOnFileOverlap = config.SerializeOnFileOverlap;
|
|
}
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
|
|
public async Task<bool> DeleteConfigAsync(string listId, CancellationToken ct = default)
|
|
{
|
|
var affected = await _context.ListConfigs
|
|
.Where(c => c.ListId == listId)
|
|
.ExecuteDeleteAsync(ct);
|
|
return affected > 0;
|
|
}
|
|
}
|