feat(ui): Listen-Einstellungen — Checkbox fuer serialize_on_file_overlap
Das Flag war bisher nur ueber MCP set_list_config erreichbar. UpdateListConfigDto fuehrt es tri-state (null = gespeicherten Wert behalten), damit nur das Listen-Modal es setzen/loeschen kann und kein anderer Aufrufer es per Omission verliert.
This commit is contained in:
@@ -94,11 +94,14 @@ public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string?
|
|||||||
|
|
||||||
public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType, bool IsManual = false, bool FindingsTracked = false);
|
public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType, bool IsManual = false, bool FindingsTracked = false);
|
||||||
|
|
||||||
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null);
|
// SerializeOnFileOverlap is tri-state on purpose: null = leave the stored flag alone. A caller that
|
||||||
|
// doesn't own the field (anything but the list-settings modal) must not be able to clear it by
|
||||||
|
// omission — SetConfigAsync copies the entity verbatim.
|
||||||
|
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, bool? SerializeOnFileOverlap = null);
|
||||||
|
|
||||||
public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
||||||
|
|
||||||
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null);
|
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, bool SerializeOnFileOverlap = false);
|
||||||
|
|
||||||
public record SeedResultDto(int Copied, int Skipped);
|
public record SeedResultDto(int Copied, int Skipped);
|
||||||
|
|
||||||
|
|||||||
@@ -371,6 +371,8 @@
|
|||||||
"manualListHint": "Neue Aufgaben in dieser Liste sind zunächst manuell: kein Einreihen, Ausführen oder Verfeinern, und die Automatik überspringt sie. Eine handgesteuerte Sitzung kannst du weiterhin öffnen.",
|
"manualListHint": "Neue Aufgaben in dieser Liste sind zunächst manuell: kein Einreihen, Ausführen oder Verfeinern, und die Automatik überspringt sie. Eine handgesteuerte Sitzung kannst du weiterhin öffnen.",
|
||||||
"findingsTracked": "Findings-Ordner .claudedo einchecken",
|
"findingsTracked": "Findings-Ordner .claudedo einchecken",
|
||||||
"findingsTrackedHint": "Aus: der Ordner bleibt über .git/info/exclude aus git heraus. An: Findings reisen mit dem Repo.",
|
"findingsTrackedHint": "Aus: der Ordner bleibt über .git/info/exclude aus git heraus. An: Findings reisen mit dem Repo.",
|
||||||
|
"serializeOnFileOverlap": "Tasks mit gleichen Dateien serialisieren",
|
||||||
|
"serializeOnFileOverlapHint": "Aus: Tasks dieser Liste laufen parallel. An: ein wartender Task, dessen deklarierter Datei-Scope einen laufenden oder review-wartenden Nachbarn überlappt, wartet statt zu starten.",
|
||||||
"sectionAgent": "AGENT",
|
"sectionAgent": "AGENT",
|
||||||
"resetAgentSettings": "Agent-Einstellungen zurücksetzen",
|
"resetAgentSettings": "Agent-Einstellungen zurücksetzen",
|
||||||
"sectionVerify": "VERIFIKATION",
|
"sectionVerify": "VERIFIKATION",
|
||||||
|
|||||||
@@ -371,6 +371,8 @@
|
|||||||
"manualListHint": "New tasks in this list start out manual: no queueing, running or refining, and automation skips them. You can still open a hand-driven session.",
|
"manualListHint": "New tasks in this list start out manual: no queueing, running or refining, and automation skips them. You can still open a hand-driven session.",
|
||||||
"findingsTracked": "Commit the .claudedo findings folder",
|
"findingsTracked": "Commit the .claudedo findings folder",
|
||||||
"findingsTrackedHint": "Off: the folder stays out of git via .git/info/exclude. On: findings travel with the repo.",
|
"findingsTrackedHint": "Off: the folder stays out of git via .git/info/exclude. On: findings travel with the repo.",
|
||||||
|
"serializeOnFileOverlap": "Serialize tasks that touch the same files",
|
||||||
|
"serializeOnFileOverlapHint": "Off: tasks in this list run in parallel. On: a queued task whose declared file scope overlaps a running or awaiting-review sibling waits instead of starting.",
|
||||||
"sectionAgent": "AGENT",
|
"sectionAgent": "AGENT",
|
||||||
"resetAgentSettings": "Reset agent settings",
|
"resetAgentSettings": "Reset agent settings",
|
||||||
"sectionVerify": "VERIFICATION",
|
"sectionVerify": "VERIFICATION",
|
||||||
|
|||||||
@@ -189,10 +189,12 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifyCommand is a List-only field owned by ListSettingsModalViewModel (not this editor,
|
// verifyCommand and serializeOnFileOverlap are List-only fields owned by
|
||||||
// which is also reused for Task scope); the caller passes it through so the single
|
// ListSettingsModalViewModel (not this editor, which is also reused for Task scope); the caller
|
||||||
// UpdateListConfig call carries the full desired row instead of clobbering it.
|
// passes them through so the single UpdateListConfig call carries the full desired row instead
|
||||||
public async System.Threading.Tasks.Task SaveAsync(string? verifyCommand = null)
|
// of clobbering it.
|
||||||
|
public async System.Threading.Tasks.Task SaveAsync(
|
||||||
|
string? verifyCommand = null, bool? serializeOnFileOverlap = null)
|
||||||
{
|
{
|
||||||
if (TargetId is null) return;
|
if (TargetId is null) return;
|
||||||
var model = string.IsNullOrWhiteSpace(Model) ? null : Model;
|
var model = string.IsNullOrWhiteSpace(Model) ? null : Model;
|
||||||
@@ -204,7 +206,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
|||||||
if (_scope == AgentConfigScope.Task)
|
if (_scope == AgentConfigScope.Task)
|
||||||
await _worker.UpdateTaskAgentSettingsAsync(new UpdateTaskAgentSettingsDto(TargetId, model, sp, ap, turns, skills));
|
await _worker.UpdateTaskAgentSettingsAsync(new UpdateTaskAgentSettingsDto(TargetId, model, sp, ap, turns, skills));
|
||||||
else
|
else
|
||||||
await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills, verifyCommand));
|
await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills, verifyCommand, serializeOnFileOverlap));
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<string>? SelectedSessionSkillNames()
|
private List<string>? SelectedSessionSkillNames()
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
// Optional post-merge verification command (build/test), run in WorkingDir after a merge
|
// Optional post-merge verification command (build/test), run in WorkingDir after a merge
|
||||||
// lands; a non-zero exit keeps the task out of Done instead of silently reporting merged.
|
// lands; a non-zero exit keeps the task out of Done instead of silently reporting merged.
|
||||||
[ObservableProperty] private string _verifyCommand = "";
|
[ObservableProperty] private string _verifyCommand = "";
|
||||||
|
// When on, the queue picker holds back a queued task whose declared scope globs overlap a
|
||||||
|
// running/awaiting-merge sibling in this list (ListConfigEntity.SerializeOnFileOverlap).
|
||||||
|
[ObservableProperty] private bool _serializeOnFileOverlap;
|
||||||
|
|
||||||
public ObservableCollection<string> CommitTypeOptions { get; } = new(CommitTypeRegistry.Types);
|
public ObservableCollection<string> CommitTypeOptions { get; } = new(CommitTypeRegistry.Types);
|
||||||
|
|
||||||
@@ -71,6 +74,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
await Agent.LoadForListAsync(listId, ct);
|
await Agent.LoadForListAsync(listId, ct);
|
||||||
var cfg = await _worker.GetListConfigAsync(listId);
|
var cfg = await _worker.GetListConfigAsync(listId);
|
||||||
VerifyCommand = cfg?.VerifyCommand ?? "";
|
VerifyCommand = cfg?.VerifyCommand ?? "";
|
||||||
|
SerializeOnFileOverlap = cfg?.SerializeOnFileOverlap ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -84,7 +88,9 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
IsManual,
|
IsManual,
|
||||||
FindingsTracked));
|
FindingsTracked));
|
||||||
|
|
||||||
await Agent.SaveAsync(string.IsNullOrWhiteSpace(VerifyCommand) ? null : VerifyCommand);
|
await Agent.SaveAsync(
|
||||||
|
string.IsNullOrWhiteSpace(VerifyCommand) ? null : VerifyCommand,
|
||||||
|
SerializeOnFileOverlap);
|
||||||
|
|
||||||
CloseAction?.Invoke();
|
CloseAction?.Invoke();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,13 @@
|
|||||||
<TextBlock Text="{loc:Tr modals.listSettings.findingsTrackedHint}"
|
<TextBlock Text="{loc:Tr modals.listSettings.findingsTrackedHint}"
|
||||||
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<CheckBox IsChecked="{Binding SerializeOnFileOverlap, Mode=TwoWay}"
|
||||||
|
Content="{loc:Tr modals.listSettings.serializeOnFileOverlap}"/>
|
||||||
|
<TextBlock Text="{loc:Tr modals.listSettings.serializeOnFileOverlapHint}"
|
||||||
|
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
+2
-2
@@ -104,8 +104,8 @@ public sealed class ConfigMcpTools
|
|||||||
|
|
||||||
// Fields this tool doesn't expose but that live on the same row. They must survive every
|
// Fields this tool doesn't expose but that live on the same row. They must survive every
|
||||||
// write here — ListRepository.SetConfigAsync copies the entity verbatim, so anything left
|
// write here — ListRepository.SetConfigAsync copies the entity verbatim, so anything left
|
||||||
// at its default would silently reset (SerializeOnFileOverlap in particular has no UI
|
// at its default would silently reset (a SerializeOnFileOverlap reset only shows up as
|
||||||
// affordance at all, so a reset is invisible until tasks stop serializing).
|
// tasks no longer serializing, long after this write).
|
||||||
var hasUnrelatedSettings = existing is not null
|
var hasUnrelatedSettings = existing is not null
|
||||||
&& (existing.SessionSkills is not null || existing.SerializeOnFileOverlap);
|
&& (existing.SessionSkills is not null || existing.SerializeOnFileOverlap);
|
||||||
|
|
||||||
|
|||||||
@@ -545,13 +545,13 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
var sessionSkills = SkillsToJson(dto.SessionSkills);
|
var sessionSkills = SkillsToJson(dto.SessionSkills);
|
||||||
var verifyCommand = dto.VerifyCommand.NullIfBlank();
|
var verifyCommand = dto.VerifyCommand.NullIfBlank();
|
||||||
|
|
||||||
// Preserve SerializeOnFileOverlap: it has no UI/hub affordance yet (set via
|
// A null SerializeOnFileOverlap means "leave it as stored" — only the list-settings modal
|
||||||
// set_list_config or directly against ListConfigEntity), so a save from this path
|
// owns that field, so every other caller must not drop it (neither by deleting the row nor
|
||||||
// must not silently drop it -- neither by deleting the row nor by overwriting it.
|
// by overwriting it: SetConfigAsync copies the entity verbatim).
|
||||||
var existing = await repo.GetConfigAsync(dto.ListId);
|
var existing = await repo.GetConfigAsync(dto.ListId);
|
||||||
var hasUnrelatedSettings = existing?.SerializeOnFileOverlap ?? false;
|
var serializeOnFileOverlap = dto.SerializeOnFileOverlap ?? existing?.SerializeOnFileOverlap ?? false;
|
||||||
|
|
||||||
if (model is null && systemPrompt is null && agentPath is null && dto.MaxTurns is null && sessionSkills is null && verifyCommand is null && !hasUnrelatedSettings)
|
if (model is null && systemPrompt is null && agentPath is null && dto.MaxTurns is null && sessionSkills is null && verifyCommand is null && !serializeOnFileOverlap)
|
||||||
{
|
{
|
||||||
await repo.DeleteConfigAsync(dto.ListId);
|
await repo.DeleteConfigAsync(dto.ListId);
|
||||||
}
|
}
|
||||||
@@ -566,7 +566,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
MaxTurns = dto.MaxTurns,
|
MaxTurns = dto.MaxTurns,
|
||||||
SessionSkills = sessionSkills,
|
SessionSkills = sessionSkills,
|
||||||
VerifyCommand = verifyCommand,
|
VerifyCommand = verifyCommand,
|
||||||
SerializeOnFileOverlap = existing?.SerializeOnFileOverlap ?? false,
|
SerializeOnFileOverlap = serializeOnFileOverlap,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,7 +579,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
var repo = new ListRepository(ctx);
|
var repo = new ListRepository(ctx);
|
||||||
var config = await repo.GetConfigAsync(listId);
|
var config = await repo.GetConfigAsync(listId);
|
||||||
if (config is null) return null;
|
if (config is null) return null;
|
||||||
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns, SkillsFromJson(config.SessionSkills), config.VerifyCommand);
|
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns, SkillsFromJson(config.SessionSkills), config.VerifyCommand, config.SerializeOnFileOverlap);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SetTaskStatusResultDto> SetTaskStatus(string taskId, string status)
|
public async Task<SetTaskStatusResultDto> SetTaskStatus(string taskId, string status)
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ using Xunit;
|
|||||||
namespace ClaudeDo.Worker.Tests.Hub;
|
namespace ClaudeDo.Worker.Tests.Hub;
|
||||||
|
|
||||||
/// UpdateListConfig's "all fields blank -> delete the row" branch used to delete unconditionally,
|
/// UpdateListConfig's "all fields blank -> delete the row" branch used to delete unconditionally,
|
||||||
/// silently dropping SerializeOnFileOverlap -- a flag with no UI/hub affordance of its own (set
|
/// silently dropping SerializeOnFileOverlap. The DTO field is tri-state: the list-settings modal
|
||||||
/// only via set_list_config or directly against ListConfigEntity).
|
/// sends an explicit true/false, every other caller sends null and must not clear the stored flag.
|
||||||
public sealed class ListConfigHubTests : IDisposable
|
public sealed class ListConfigHubTests : IDisposable
|
||||||
{
|
{
|
||||||
private readonly DbFixture _db = new();
|
private readonly DbFixture _db = new();
|
||||||
@@ -98,4 +98,29 @@ public sealed class ListConfigHubTests : IDisposable
|
|||||||
Assert.Equal("opus", config!.Model);
|
Assert.Equal("opus", config!.Model);
|
||||||
Assert.True(config.SerializeOnFileOverlap);
|
Assert.True(config.SerializeOnFileOverlap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateListConfig_ExplicitFalse_ClearsFlagAndDeletesOtherwiseEmptyRow()
|
||||||
|
{
|
||||||
|
var hub = CreateHub();
|
||||||
|
var listId = await SeedListAsync();
|
||||||
|
await SeedConfigAsync(listId, serializeOnFileOverlap: true);
|
||||||
|
|
||||||
|
await hub.UpdateListConfig(new UpdateListConfigDto(listId, null, null, null, SerializeOnFileOverlap: false));
|
||||||
|
|
||||||
|
Assert.Null(await GetConfigAsync(listId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateListConfig_ExplicitTrue_SetsFlagOnOtherwiseEmptyRow()
|
||||||
|
{
|
||||||
|
var hub = CreateHub();
|
||||||
|
var listId = await SeedListAsync();
|
||||||
|
|
||||||
|
await hub.UpdateListConfig(new UpdateListConfigDto(listId, null, null, null, SerializeOnFileOverlap: true));
|
||||||
|
|
||||||
|
var config = await GetConfigAsync(listId);
|
||||||
|
Assert.NotNull(config);
|
||||||
|
Assert.True(config!.SerializeOnFileOverlap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user