chore(claude-do): merge Customizable Spalten Anzahl in MIssion controll
Changelog / changelog (push) Successful in 2s
Release / release (push) Successful in 43s

ClaudeDo-Task: 99bfb985efb64c02ba3f27b53eae790d
This commit is contained in:
Mika Kuns
2026-09-30 10:50:21 +02:00
8 changed files with 99 additions and 8 deletions
+2 -1
View File
@@ -214,7 +214,8 @@ sealed class Program
new MissionControlViewModel( new MissionControlViewModel(
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(), sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
sp.GetRequiredService<IWorkerClient>(), sp.GetRequiredService<IWorkerClient>(),
sp.GetRequiredService<UsagePillViewModel>())); sp.GetRequiredService<UsagePillViewModel>(),
sp.GetRequiredService<AppSettings>()));
sc.AddSingleton<IslandsShellViewModel>(sp => sc.AddSingleton<IslandsShellViewModel>(sp =>
{ {
var shell = ActivatorUtilities.CreateInstance<IslandsShellViewModel>(sp); var shell = ActivatorUtilities.CreateInstance<IslandsShellViewModel>(sp);
@@ -386,6 +386,8 @@
"briefOpenFailed": "Brief konnte nicht geöffnet werden: {0}", "briefOpenFailed": "Brief konnte nicht geöffnet werden: {0}",
"focusMode": "Fokus", "focusMode": "Fokus",
"overviewMode": "Übersicht", "overviewMode": "Übersicht",
"columns": "Spalten",
"columnsAuto": "Auto",
"closeSession": "Sitzung hart beenden", "closeSession": "Sitzung hart beenden",
"closeSessionConfirm": "Laufende Sitzung beenden? Der Claude-Prozess wird abgebrochen.", "closeSessionConfirm": "Laufende Sitzung beenden? Der Claude-Prozess wird abgebrochen.",
"conptyLaunchFailed": "Interaktive Sitzung konnte nicht geöffnet werden: {0}", "conptyLaunchFailed": "Interaktive Sitzung konnte nicht geöffnet werden: {0}",
@@ -386,6 +386,8 @@
"briefOpenFailed": "Couldn't open the brief: {0}", "briefOpenFailed": "Couldn't open the brief: {0}",
"focusMode": "Focus", "focusMode": "Focus",
"overviewMode": "Overview", "overviewMode": "Overview",
"columns": "Columns",
"columnsAuto": "Auto",
"closeSession": "Kill session", "closeSession": "Kill session",
"closeSessionConfirm": "End the running session? The Claude process will be aborted.", "closeSessionConfirm": "End the running session? The Claude process will be aborted.",
"conptyLaunchFailed": "Couldn't open interactive session: {0}", "conptyLaunchFailed": "Couldn't open interactive session: {0}",
+3
View File
@@ -16,6 +16,9 @@ public sealed class AppSettings
public string DiffViewMode { get; set; } = "unified"; public string DiffViewMode { get; set; } = "unified";
public bool DiffWrapLines { get; set; } public bool DiffWrapLines { get; set; }
/// <summary>Mission Control overview column count; 0 = auto (derived from the pane count).</summary>
public int MissionControlColumns { get; set; }
private static readonly string DefaultConfigPath = Paths.Expand("~/.claudeDo/ui.config.json"); private static readonly string DefaultConfigPath = Paths.Expand("~/.claudeDo/ui.config.json");
/// Where this instance persists. Instance-level (not static) so tests can redirect it — /// Where this instance persists. Instance-level (not static) so tests can redirect it —
@@ -19,6 +19,14 @@ public sealed class HexBrushConverter : OneWayConverter
: Avalonia.Application.Current?.TryGetResource(parameter as string ?? "", null, out var res) == true ? res : null; : Avalonia.Application.Current?.TryGetResource(parameter as string ?? "", null, out var res) == true ? res : null;
} }
/// <summary>True when the value's string form equals the parameter (segmented-switch active state).</summary>
public sealed class EqualsParameterConverter : OneWayConverter
{
public static EqualsParameterConverter Instance { get; } = new();
public override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
=> Equals(value?.ToString(), parameter as string);
}
public sealed class UpperCaseConverter : OneWayConverter public sealed class UpperCaseConverter : OneWayConverter
{ {
public static UpperCaseConverter Instance { get; } = new(); public static UpperCaseConverter Instance { get; } = new();
@@ -18,6 +18,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
{ {
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory; private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private readonly IWorkerClient _worker; private readonly IWorkerClient _worker;
private readonly AppSettings? _settings;
private readonly Action<string, string, DateTime> _onTaskStarted; private readonly Action<string, string, DateTime> _onTaskStarted;
private readonly Action<string, string, string, DateTime> _onTaskFinished; private readonly Action<string, string, string, DateTime> _onTaskFinished;
private readonly Action<string> _onTaskUpdated; private readonly Action<string> _onTaskUpdated;
@@ -44,6 +45,9 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
[ObservableProperty] private int _columnCount = 1; [ObservableProperty] private int _columnCount = 1;
/// <summary>User-picked column count for the overview grid; 0 = auto (derived from the pane count).</summary>
[ObservableProperty] private int _columnPreference;
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(LayoutToggleLabel))] [NotifyPropertyChangedFor(nameof(LayoutToggleLabel))]
private bool _isFocusMode; private bool _isFocusMode;
@@ -94,11 +98,13 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
public UsagePillViewModel UsagePill { get; } public UsagePillViewModel UsagePill { get; }
public MissionControlViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker, UsagePillViewModel usagePill) public MissionControlViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker, UsagePillViewModel usagePill, AppSettings? settings = null)
{ {
_dbFactory = dbFactory; _dbFactory = dbFactory;
_worker = worker; _worker = worker;
UsagePill = usagePill; UsagePill = usagePill;
_settings = settings;
_columnPreference = Math.Clamp(settings?.MissionControlColumns ?? 0, 0, MaxColumns);
ConPtySessions.CollectionChanged += OnConPtySessionsChanged; ConPtySessions.CollectionChanged += OnConPtySessionsChanged;
Panes.CollectionChanged += OnPanesChanged; Panes.CollectionChanged += OnPanesChanged;
@@ -334,6 +340,32 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
[RelayCommand] [RelayCommand]
private void ToggleLayout() => IsFocusMode = !IsFocusMode; private void ToggleLayout() => IsFocusMode = !IsFocusMode;
/// <summary>Upper bound of the column picker.</summary>
public const int MaxColumns = 4;
/// <summary>Sets the column preference from the header picker ("0" = auto) and persists it.</summary>
[RelayCommand]
private void SetColumns(string value) => ColumnPreference = Math.Clamp(int.Parse(value), 0, MaxColumns);
partial void OnColumnPreferenceChanged(int value)
{
UpdateColumnCount();
if (_settings is null) return;
_settings.MissionControlColumns = value;
try { _settings.Save(); }
catch (Exception ex) { ErrorReported?.Invoke(ex.Message); }
}
// Never more columns than panes, so a wide preference doesn't leave empty cells.
private void UpdateColumnCount() => ColumnCount = ColumnPreference > 0
? Math.Max(1, Math.Min(ColumnPreference, Panes.Count))
: Panes.Count switch
{
<= 1 => 1,
<= 4 => 2,
_ => 3,
};
// Fetches the launch spec for a task's worktree and hosts an embedded ConPTY session as a // Fetches the launch spec for a task's worktree and hosts an embedded ConPTY session as a
// Command Center pane (task-based only). // Command Center pane (task-based only).
public async System.Threading.Tasks.Task OpenConPtySessionAsync(string taskId) public async System.Threading.Tasks.Task OpenConPtySessionAsync(string taskId)
@@ -716,12 +748,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
private void OnPanesChanged(object? sender, NotifyCollectionChangedEventArgs e) private void OnPanesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
ColumnCount = Panes.Count switch UpdateColumnCount();
{
<= 1 => 1,
<= 4 => 2,
_ => 3,
};
OnPropertyChanged(nameof(HasPanes)); OnPropertyChanged(nameof(HasPanes));
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems?[0] is ConPtyPaneViewModel added) if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems?[0] is ConPtyPaneViewModel added)
FocusedPane = added; FocusedPane = added;
@@ -102,6 +102,27 @@
</Flyout> </Flyout>
</Button.Flyout> </Button.Flyout>
</Button> </Button>
<Border Classes="segmented" VerticalAlignment="Center"
IsVisible="{Binding !IsFocusMode}"
ToolTip.Tip="{loc:Tr missionControl.columns}">
<StackPanel Orientation="Horizontal">
<Button Classes="segment" Content="{loc:Tr missionControl.columnsAuto}"
Classes.active="{Binding ColumnPreference, Converter={x:Static conv:EqualsParameterConverter.Instance}, ConverterParameter=0}"
Command="{Binding SetColumnsCommand}" CommandParameter="0"/>
<Button Classes="segment" Content="1"
Classes.active="{Binding ColumnPreference, Converter={x:Static conv:EqualsParameterConverter.Instance}, ConverterParameter=1}"
Command="{Binding SetColumnsCommand}" CommandParameter="1"/>
<Button Classes="segment" Content="2"
Classes.active="{Binding ColumnPreference, Converter={x:Static conv:EqualsParameterConverter.Instance}, ConverterParameter=2}"
Command="{Binding SetColumnsCommand}" CommandParameter="2"/>
<Button Classes="segment" Content="3"
Classes.active="{Binding ColumnPreference, Converter={x:Static conv:EqualsParameterConverter.Instance}, ConverterParameter=3}"
Command="{Binding SetColumnsCommand}" CommandParameter="3"/>
<Button Classes="segment" Content="4"
Classes.active="{Binding ColumnPreference, Converter={x:Static conv:EqualsParameterConverter.Instance}, ConverterParameter=4}"
Command="{Binding SetColumnsCommand}" CommandParameter="4"/>
</StackPanel>
</Border>
<Button Classes="icon-btn" <Button Classes="icon-btn"
Command="{Binding ToggleLayoutCommand}" Command="{Binding ToggleLayoutCommand}"
ToolTip.Tip="{Binding LayoutToggleLabel}"> ToolTip.Tip="{Binding LayoutToggleLabel}">
@@ -491,6 +491,33 @@ public class MissionControlViewModelTests : IDisposable
Assert.Equal(2, vm.ColumnCount); Assert.Equal(2, vm.ColumnCount);
} }
[Fact]
public async Task SetColumns_OverridesAutoCapsAtPaneCountAndPersists()
{
var path = Path.Combine(Path.GetTempPath(), $"claudedo_mc_cfg_{Guid.NewGuid():N}.json");
try
{
var worker = new FakeWorker();
using var vm = new MissionControlViewModel(new TestDbFactory(NewContext), worker,
new UsagePillViewModel(worker), new AppSettings { ConfigPath = path });
for (var i = 1; i <= 4; i++) await vm.OpenConPtySessionAsync($"t{i}");
Assert.Equal(2, vm.ColumnCount); // auto: 4 panes -> 2x2
vm.SetColumnsCommand.Execute("3");
Assert.Equal(3, vm.ColumnCount);
vm.SetColumnsCommand.Execute("4");
vm.Panes[0].CloseCommand.Execute(null);
Assert.Equal(3, vm.ColumnCount); // never more columns than panes
Assert.Equal(4, AppSettings.Load(path).MissionControlColumns);
vm.SetColumnsCommand.Execute("0");
Assert.Equal(2, vm.ColumnCount);
}
finally { try { File.Delete(path); } catch { } }
}
[Fact] [Fact]
public async Task CloseConPtySession_RemovesFromConPtySessionsAndPanes() public async Task CloseConPtySession_RemovesFromConPtySessionsAndPanes()
{ {