diff --git a/src/ClaudeDo.App/Program.cs b/src/ClaudeDo.App/Program.cs index 1ac75e47..f0b8437f 100644 --- a/src/ClaudeDo.App/Program.cs +++ b/src/ClaudeDo.App/Program.cs @@ -214,7 +214,8 @@ sealed class Program new MissionControlViewModel( sp.GetRequiredService>(), sp.GetRequiredService(), - sp.GetRequiredService())); + sp.GetRequiredService(), + sp.GetRequiredService())); sc.AddSingleton(sp => { var shell = ActivatorUtilities.CreateInstance(sp); diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index fcaddaa1..37b0bba4 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -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}", diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index cb68d996..e45e9de5 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -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}", diff --git a/src/ClaudeDo.Ui/AppSettings.cs b/src/ClaudeDo.Ui/AppSettings.cs index 863b5cd7..29ecbb89 100644 --- a/src/ClaudeDo.Ui/AppSettings.cs +++ b/src/ClaudeDo.Ui/AppSettings.cs @@ -16,6 +16,9 @@ public sealed class AppSettings public string DiffViewMode { get; set; } = "unified"; public bool DiffWrapLines { get; set; } + /// Mission Control overview column count; 0 = auto (derived from the pane count). + 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 — diff --git a/src/ClaudeDo.Ui/Converters/SimpleConverters.cs b/src/ClaudeDo.Ui/Converters/SimpleConverters.cs index edf2d914..2b7e22e9 100644 --- a/src/ClaudeDo.Ui/Converters/SimpleConverters.cs +++ b/src/ClaudeDo.Ui/Converters/SimpleConverters.cs @@ -19,6 +19,14 @@ public sealed class HexBrushConverter : OneWayConverter : Avalonia.Application.Current?.TryGetResource(parameter as string ?? "", null, out var res) == true ? res : null; } +/// True when the value's string form equals the parameter (segmented-switch active state). +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(); diff --git a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs index 42899d82..1618412e 100644 --- a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs @@ -18,6 +18,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable { private readonly IDbContextFactory _dbFactory; private readonly IWorkerClient _worker; + private readonly AppSettings? _settings; private readonly Action _onTaskStarted; private readonly Action _onTaskFinished; private readonly Action _onTaskUpdated; @@ -44,6 +45,9 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable [ObservableProperty] private int _columnCount = 1; + /// User-picked column count for the overview grid; 0 = auto (derived from the pane count). + [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 dbFactory, IWorkerClient worker, UsagePillViewModel usagePill) + public MissionControlViewModel(IDbContextFactory 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; + /// Upper bound of the column picker. + public const int MaxColumns = 4; + + /// Sets the column preference from the header picker ("0" = auto) and persists it. + [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; diff --git a/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml b/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml index 9096d5fe..8257f51b 100644 --- a/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml +++ b/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml @@ -102,6 +102,27 @@ + + +