app_settings bekommt power_use_enabled (Master-Schalter, wird im Usage-Monitor gesetzt) und list_handler_max_handoffs (Default 4 = zwei Merge-Runden). list_config bekommt power_use_allowed als Opt-in pro Liste. Ein Handler-Run laeuft nur unbeaufsichtigt, wenn BEIDE Schalter gesetzt sind. Damit kann der globale Schalter ueber Nacht an bleiben, ohne dass der Handler einer nicht zugestimmten Liste sein Verhalten aendert. UpdateListConfigDto.PowerUseAllowed ist bewusst bool? (tri-state, null = gespeicherten Wert behalten), analog zu TicketProjectId: SetConfigAsync kopiert verbatim, ein non-nullable bool haette jedem fremden Writer erlaubt, das Opt-in per Auslassung zu widerrufen. Der Test dazu nagelt genau das fest. Migration ist drei reine ALTER TABLE ADD COLUMN, kein Table-Rebuild.
106 lines
3.9 KiB
C#
106 lines
3.9 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.PermissionMode = config.PermissionMode;
|
|
existing.TicketProjectId = config.TicketProjectId;
|
|
existing.PowerUseAllowed = config.PowerUseAllowed;
|
|
}
|
|
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;
|
|
}
|
|
}
|