feat(mission-control): column picker for the overview grid

Auto keeps the pane-count heuristic; 1-4 fixes the column count (capped at
the pane count). Persisted to ui.config.json as MissionControlColumns.
This commit is contained in:
Mika Kuns
2026-09-30 10:39:23 +02:00
parent 0d029717f8
commit a7c3d18533
8 changed files with 99 additions and 8 deletions
+2 -1
View File
@@ -214,7 +214,8 @@ sealed class Program
new MissionControlViewModel(
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
sp.GetRequiredService<IWorkerClient>(),
sp.GetRequiredService<UsagePillViewModel>()));
sp.GetRequiredService<UsagePillViewModel>(),
sp.GetRequiredService<AppSettings>()));
sc.AddSingleton<IslandsShellViewModel>(sp =>
{
var shell = ActivatorUtilities.CreateInstance<IslandsShellViewModel>(sp);
@@ -386,6 +386,8 @@
"briefOpenFailed": "Brief konnte nicht geöffnet werden: {0}",
"focusMode": "Fokus",
"overviewMode": "Übersicht",
"columns": "Spalten",
"columnsAuto": "Auto",
"closeSession": "Sitzung hart beenden",
"closeSessionConfirm": "Laufende Sitzung beenden? Der Claude-Prozess wird abgebrochen.",
"conptyLaunchFailed": "Interaktive Sitzung konnte nicht geöffnet werden: {0}",
@@ -386,6 +386,8 @@
"briefOpenFailed": "Couldn't open the brief: {0}",
"focusMode": "Focus",
"overviewMode": "Overview",
"columns": "Columns",
"columnsAuto": "Auto",
"closeSession": "Kill session",
"closeSessionConfirm": "End the running session? The Claude process will be aborted.",
"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 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");
/// 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;
}
/// <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 static UpperCaseConverter Instance { get; } = new();
@@ -18,6 +18,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
{
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private readonly IWorkerClient _worker;
private readonly AppSettings? _settings;
private readonly Action<string, string, DateTime> _onTaskStarted;
private readonly Action<string, string, string, DateTime> _onTaskFinished;
private readonly Action<string> _onTaskUpdated;
@@ -44,6 +45,9 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
[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]
[NotifyPropertyChangedFor(nameof(LayoutToggleLabel))]
private bool _isFocusMode;
@@ -94,11 +98,13 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
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;
_worker = worker;
UsagePill = usagePill;
_settings = settings;
_columnPreference = Math.Clamp(settings?.MissionControlColumns ?? 0, 0, MaxColumns);
ConPtySessions.CollectionChanged += OnConPtySessionsChanged;
Panes.CollectionChanged += OnPanesChanged;
@@ -334,6 +340,32 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
[RelayCommand]
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
// Command Center pane (task-based only).
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)
{
ColumnCount = Panes.Count switch
{
<= 1 => 1,
<= 4 => 2,
_ => 3,
};
UpdateColumnCount();
OnPropertyChanged(nameof(HasPanes));
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems?[0] is ConPtyPaneViewModel added)
FocusedPane = added;
@@ -102,6 +102,27 @@
</Flyout>
</Button.Flyout>
</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"
Command="{Binding ToggleLayoutCommand}"
ToolTip.Tip="{Binding LayoutToggleLabel}">