feat(ui): base branch field in list settings, real branch names in dialogs

This commit is contained in:
mika kuns
2026-10-01 10:48:32 +02:00
parent 69c7803bdf
commit ae845bf06b
7 changed files with 44 additions and 8 deletions
@@ -471,6 +471,9 @@
"folderColorHint": "Färbt den Ordnernamen und seine Führungslinie in der Seitenleiste.", "folderColorHint": "Färbt den Ordnernamen und seine Führungslinie in der Seitenleiste.",
"workingDirectory": "Arbeitsverzeichnis", "workingDirectory": "Arbeitsverzeichnis",
"workingDirectoryPlaceholder": "(keines)", "workingDirectoryPlaceholder": "(keines)",
"baseBranch": "Basis-Branch",
"baseBranchPlaceholder": "(ausgecheckter Branch)",
"baseBranchHint": "Neue Task-Branches starten hier, und freigegebene Arbeit wird hierhin gemergt, z.B. dev. Leer = der gerade ausgecheckte Branch.",
"browse": "Durchsuchen...", "browse": "Durchsuchen...",
"defaultCommitType": "Standard-Commit-Typ", "defaultCommitType": "Standard-Commit-Typ",
"manualList": "Manuelle Liste (Erinnerungen)", "manualList": "Manuelle Liste (Erinnerungen)",
@@ -471,6 +471,9 @@
"folderColorHint": "Tints the folder name and its guide line in the sidebar.", "folderColorHint": "Tints the folder name and its guide line in the sidebar.",
"workingDirectory": "Working directory", "workingDirectory": "Working directory",
"workingDirectoryPlaceholder": "(none)", "workingDirectoryPlaceholder": "(none)",
"baseBranch": "Base branch",
"baseBranchPlaceholder": "(checked-out branch)",
"baseBranchHint": "New task branches start here and approved work merges back here, e.g. dev. Leave empty to use whatever branch is checked out.",
"browse": "Browse...", "browse": "Browse...",
"defaultCommitType": "Default commit type", "defaultCommitType": "Default commit type",
"manualList": "Manual list (reminders)", "manualList": "Manual list (reminders)",
@@ -208,13 +208,13 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
catch { } catch { }
} }
// verifyCommand, ticketProjectId and powerUseAllowed are List-only fields owned by // verifyCommand, ticketProjectId, powerUseAllowed and baseBranch are List-only fields owned by
// ListSettingsModalViewModel (not this editor, which is also reused for Task scope); the caller // ListSettingsModalViewModel (not this editor, which is also reused for Task scope); the caller
// passes them through so the single UpdateListConfig call carries the full desired row instead // passes them through so the single UpdateListConfig call carries the full desired row instead
// of clobbering it. powerUseAllowed stays null-by-default for the same reason ticketProjectId // of clobbering it. powerUseAllowed stays null-by-default for the same reason ticketProjectId
// does: a caller that doesn't own it must not revoke the list's opt-in by omission. // does: a caller that doesn't own it must not revoke the list's opt-in by omission.
public async System.Threading.Tasks.Task SaveAsync( public async System.Threading.Tasks.Task SaveAsync(
string? verifyCommand = null, int? ticketProjectId = null, bool? powerUseAllowed = null) string? verifyCommand = null, int? ticketProjectId = null, bool? powerUseAllowed = null, string? baseBranch = 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;
@@ -227,7 +227,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, permission)); await _worker.UpdateTaskAgentSettingsAsync(new UpdateTaskAgentSettingsDto(TargetId, model, sp, ap, turns, skills, permission));
else else
await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills, verifyCommand, permission, ticketProjectId, powerUseAllowed)); await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills, verifyCommand, permission, ticketProjectId, powerUseAllowed, baseBranch));
} }
private List<string>? SelectedSessionSkillNames() private List<string>? SelectedSessionSkillNames()
@@ -667,7 +667,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
WorktreeBaseCommit = entity.Worktree?.BaseCommit ?? entity.HandlerBaseCommit; WorktreeBaseCommit = entity.Worktree?.BaseCommit ?? entity.HandlerBaseCommit;
WorktreeHeadCommit = entity.Worktree?.HeadCommit ?? entity.HandlerHeadCommit; WorktreeHeadCommit = entity.Worktree?.HeadCommit ?? entity.HandlerHeadCommit;
WorktreeStateLabel = entity.Worktree?.State.ToString(); WorktreeStateLabel = entity.Worktree?.State.ToString();
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null; BranchLine = entity.Worktree?.BranchName;
var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat); var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat);
DiffAdditions = add; DiffAdditions = add;
DiffDeletions = del; DiffDeletions = del;
@@ -930,7 +930,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
WorktreeBaseCommit = entity.Worktree?.BaseCommit ?? entity.HandlerBaseCommit; WorktreeBaseCommit = entity.Worktree?.BaseCommit ?? entity.HandlerBaseCommit;
WorktreeHeadCommit = entity.Worktree?.HeadCommit ?? entity.HandlerHeadCommit; WorktreeHeadCommit = entity.Worktree?.HeadCommit ?? entity.HandlerHeadCommit;
WorktreeStateLabel = entity.Worktree?.State.ToString(); WorktreeStateLabel = entity.Worktree?.State.ToString();
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null; BranchLine = entity.Worktree?.BranchName;
if (Task is { } row && entity.Worktree?.DiffStat is { } stat) if (Task is { } row && entity.Worktree?.DiffStat is { } stat)
row.DiffStat = stat; row.DiffStat = stat;
var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat); var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat);
@@ -1220,7 +1220,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
if (Task == null) return; if (Task == null) return;
if (Dialogs == null) return; if (Dialogs == null) return;
var branchName = $"claudedo/{Task.Id.Replace("-", "")}"; var branchName = BranchLine ?? "the task branch";
var ok = await Dialogs.ConfirmAsync( var ok = await Dialogs.ConfirmAsync(
$"Reset and retry?\nThis discards branch {branchName} (and uncommitted changes), then queues the task to run from the beginning."); $"Reset and retry?\nThis discards branch {branchName} (and uncommitted changes), then queues the task to run from the beginning.");
if (!ok) return; if (!ok) return;
@@ -1382,7 +1382,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
private async System.Threading.Tasks.Task ResetReviewAsync() private async System.Threading.Tasks.Task ResetReviewAsync()
{ {
if (Task is null || !_worker.IsConnected || Dialogs is null) return; if (Task is null || !_worker.IsConnected || Dialogs is null) return;
var branchName = $"claudedo/{Task.Id.Replace("-", "")}"; var branchName = BranchLine ?? "the task branch";
var ok = await Dialogs.ConfirmAsync( var ok = await Dialogs.ConfirmAsync(
$"Reset working tree?\nThis discards branch {branchName} (and all changes) and returns the task to Idle."); $"Reset working tree?\nThis discards branch {branchName} (and all changes) and returns the task to Idle.");
if (!ok) return; if (!ok) return;
@@ -59,6 +59,8 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
// harvests new work, longer handoff chain) when this AND the global switch in Settings → // harvests new work, longer handoff chain) when this AND the global switch in Settings →
// Execution are both on. Off by default — the global switch alone must never change a list's behaviour. // Execution are both on. Off by default — the global switch alone must never change a list's behaviour.
[ObservableProperty] private bool _powerUseAllowed; [ObservableProperty] private bool _powerUseAllowed;
// Basis neuer Worktrees und Default-Merge-Ziel; leer = ausgecheckter Branch.
[ObservableProperty] private string _baseBranch = "";
// Dropdown hidden entirely when no ticket-system base URL is configured (Settings → Files tab). // Dropdown hidden entirely when no ticket-system base URL is configured (Settings → Files tab).
// Icon key into the UI icon library (IconKeyConverter -> "Icon.<key>" StreamGeometry). // Icon key into the UI icon library (IconKeyConverter -> "Icon.<key>" StreamGeometry).
[ObservableProperty] private string _icon = DefaultIcon; [ObservableProperty] private string _icon = DefaultIcon;
@@ -275,6 +277,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
cfg = Agent.LastLoadedListConfig; cfg = Agent.LastLoadedListConfig;
VerifyCommand = cfg?.VerifyCommand ?? ""; VerifyCommand = cfg?.VerifyCommand ?? "";
PowerUseAllowed = cfg?.PowerUseAllowed ?? false; PowerUseAllowed = cfg?.PowerUseAllowed ?? false;
BaseBranch = cfg?.BaseBranch ?? "";
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -377,7 +380,9 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
await Agent.SaveAsync( await Agent.SaveAsync(
string.IsNullOrWhiteSpace(VerifyCommand) ? null : VerifyCommand, string.IsNullOrWhiteSpace(VerifyCommand) ? null : VerifyCommand,
ticketProjectId, ticketProjectId,
PowerUseAllowed); PowerUseAllowed,
// Always explicit too: "" clears the stored base branch.
BaseBranch.Trim());
Saved = true; Saved = true;
CloseAction?.Invoke(); CloseAction?.Invoke();
@@ -124,6 +124,14 @@
Foreground="{DynamicResource StatusErrorBrush}" FontSize="12" TextWrapping="Wrap"/> Foreground="{DynamicResource StatusErrorBrush}" FontSize="12" TextWrapping="Wrap"/>
</StackPanel> </StackPanel>
<StackPanel Spacing="4" IsVisible="{Binding !IsFolder}">
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.baseBranch}"/>
<TextBox Text="{Binding BaseBranch, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
PlaceholderText="{loc:Tr modals.listSettings.baseBranchPlaceholder}"/>
<TextBlock Text="{loc:Tr modals.listSettings.baseBranchHint}"
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
</StackPanel>
<StackPanel Spacing="4" IsVisible="{Binding !IsFolder}"> <StackPanel Spacing="4" IsVisible="{Binding !IsFolder}">
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.defaultCommitType}"/> <TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.defaultCommitType}"/>
<ComboBox ItemsSource="{Binding CommitTypeOptions}" <ComboBox ItemsSource="{Binding CommitTypeOptions}"
@@ -165,6 +165,23 @@ public class ListSettingsModalViewModelTests
Assert.Equal(7, worker.SavedListConfig!.TicketProjectId); Assert.Equal(7, worker.SavedListConfig!.TicketProjectId);
} }
[Fact]
public async Task Save_sends_BaseBranch_explicitly()
{
var worker = new CountingWorker { Cfg = new ListConfigDto("opus", null, null, BaseBranch: "dev") };
var vm = new ListSettingsModalViewModel(worker, NewFactory());
await vm.LoadAsync("l1", "List One", "/repo", "feat");
Assert.Equal("dev", vm.BaseBranch);
await vm.SaveCommand.ExecuteAsync(null);
Assert.Equal("dev", worker.SavedListConfig!.BaseBranch);
// "" (not null) so clearing the field really clears the stored branch.
vm.BaseBranch = " ";
await vm.SaveCommand.ExecuteAsync(null);
Assert.Equal("", worker.SavedListConfig!.BaseBranch);
}
[Fact] [Fact]
public async Task LoadAsync_CoreLoadThrows_BlocksSaveAndDoesNotOverwriteConfig() public async Task LoadAsync_CoreLoadThrows_BlocksSaveAndDoesNotOverwriteConfig()
{ {