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:
@@ -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}",
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user