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.",
"workingDirectory": "Arbeitsverzeichnis",
"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...",
"defaultCommitType": "Standard-Commit-Typ",
"manualList": "Manuelle Liste (Erinnerungen)",
@@ -471,6 +471,9 @@
"folderColorHint": "Tints the folder name and its guide line in the sidebar.",
"workingDirectory": "Working directory",
"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...",
"defaultCommitType": "Default commit type",
"manualList": "Manual list (reminders)",
@@ -208,13 +208,13 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
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
// 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
// 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(
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;
var model = string.IsNullOrWhiteSpace(Model) ? null : Model;
@@ -227,7 +227,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
if (_scope == AgentConfigScope.Task)
await _worker.UpdateTaskAgentSettingsAsync(new UpdateTaskAgentSettingsDto(TargetId, model, sp, ap, turns, skills, permission));
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()
@@ -667,7 +667,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
WorktreeBaseCommit = entity.Worktree?.BaseCommit ?? entity.HandlerBaseCommit;
WorktreeHeadCommit = entity.Worktree?.HeadCommit ?? entity.HandlerHeadCommit;
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);
DiffAdditions = add;
DiffDeletions = del;
@@ -930,7 +930,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
WorktreeBaseCommit = entity.Worktree?.BaseCommit ?? entity.HandlerBaseCommit;
WorktreeHeadCommit = entity.Worktree?.HeadCommit ?? entity.HandlerHeadCommit;
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)
row.DiffStat = stat;
var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat);
@@ -1220,7 +1220,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
if (Task == null) return;
if (Dialogs == null) return;
var branchName = $"claudedo/{Task.Id.Replace("-", "")}";
var branchName = BranchLine ?? "the task branch";
var ok = await Dialogs.ConfirmAsync(
$"Reset and retry?\nThis discards branch {branchName} (and uncommitted changes), then queues the task to run from the beginning.");
if (!ok) return;
@@ -1382,7 +1382,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
private async System.Threading.Tasks.Task ResetReviewAsync()
{
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(
$"Reset working tree?\nThis discards branch {branchName} (and all changes) and returns the task to Idle.");
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 →
// Execution are both on. Off by default — the global switch alone must never change a list's behaviour.
[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).
// Icon key into the UI icon library (IconKeyConverter -> "Icon.<key>" StreamGeometry).
[ObservableProperty] private string _icon = DefaultIcon;
@@ -275,6 +277,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
cfg = Agent.LastLoadedListConfig;
VerifyCommand = cfg?.VerifyCommand ?? "";
PowerUseAllowed = cfg?.PowerUseAllowed ?? false;
BaseBranch = cfg?.BaseBranch ?? "";
}
catch (Exception ex)
{
@@ -377,7 +380,9 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
await Agent.SaveAsync(
string.IsNullOrWhiteSpace(VerifyCommand) ? null : VerifyCommand,
ticketProjectId,
PowerUseAllowed);
PowerUseAllowed,
// Always explicit too: "" clears the stored base branch.
BaseBranch.Trim());
Saved = true;
CloseAction?.Invoke();
@@ -124,6 +124,14 @@
Foreground="{DynamicResource StatusErrorBrush}" FontSize="12" TextWrapping="Wrap"/>
</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}">
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.defaultCommitType}"/>
<ComboBox ItemsSource="{Binding CommitTypeOptions}"
@@ -165,6 +165,23 @@ public class ListSettingsModalViewModelTests
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]
public async Task LoadAsync_CoreLoadThrows_BlocksSaveAndDoesNotOverwriteConfig()
{