From 3ea48ff76d5c4eaeb5ae84920d9d7a09e28794d2 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Wed, 5 Aug 2026 15:29:00 +0200 Subject: [PATCH] feat(mission-control): make overview panes and queue strip individually resizable Replace the UniformGrid (all tiles forced to equal size) with a real Grid + GridSplitters built in code-behind per pane count/column count, so panes resize individually. The queue strip's fixed 210px column becomes a drag-resizable Grid column (min 160px) that collapses to 0 when nothing is queued, replacing the old dock-based fixed width. --- .../MissionControl/MissionControlView.axaml | 165 ++++++++++-------- .../MissionControlView.axaml.cs | 139 +++++++++++++++ 2 files changed, 228 insertions(+), 76 deletions(-) diff --git a/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml b/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml index 0fb052d2..02ae99f5 100644 --- a/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml +++ b/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml @@ -51,85 +51,98 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + HorizontalAlignment="Center" VerticalAlignment="Center" /> + - - - - - - - - - - + - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml.cs b/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml.cs index 34c1c6d4..2f544297 100644 --- a/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml.cs @@ -1,5 +1,10 @@ +using System; +using System.Collections.Specialized; +using System.ComponentModel; using Avalonia.Controls; using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; using Avalonia.Platform.Storage; using ClaudeDo.Ui.ViewModels; @@ -7,9 +12,20 @@ namespace ClaudeDo.Ui.Views.MissionControl; public partial class MissionControlView : UserControl { + private const double SplitterSize = 6; + private const double DefaultQueueWidth = 210; + private const double MinQueueWidth = 160; + private const double MinPaneWidth = 220; + private const double MinPaneHeight = 140; + + private MissionControlViewModel? _vm; + private GridLength _queueWidth = new(DefaultQueueWidth, GridUnitType.Pixel); + public MissionControlView() { InitializeComponent(); + DataContextChanged += OnDataContextChanged; + QueueSplitter.DragCompleted += (_, _) => _queueWidth = BodyGrid.ColumnDefinitions[2].Width; } // Ad-hoc ConPTY session: the view owns the folder picker, the VM only takes the chosen path. @@ -28,4 +44,127 @@ public partial class MissionControlView : UserControl await vm.OpenAdHocConPtySessionAsync(folders[0].Path.LocalPath); } + + private void OnDataContextChanged(object? sender, EventArgs e) + { + if (_vm is not null) + { + _vm.Panes.CollectionChanged -= OnPanesChanged; + _vm.PropertyChanged -= OnViewModelPropertyChanged; + } + + _vm = DataContext as MissionControlViewModel; + + if (_vm is not null) + { + _vm.Panes.CollectionChanged += OnPanesChanged; + _vm.PropertyChanged += OnViewModelPropertyChanged; + } + + RebuildOverviewGrid(); + UpdateQueueColumn(); + } + + private void OnPanesChanged(object? sender, NotifyCollectionChangedEventArgs e) => RebuildOverviewGrid(); + + private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(MissionControlViewModel.ColumnCount)) + RebuildOverviewGrid(); + else if (e.PropertyName == nameof(MissionControlViewModel.HasQueued)) + UpdateQueueColumn(); + } + + // The queue column is Pixel-widthed (so the splitter can drag it) — when nothing is + // queued it collapses to 0 instead of leaving a blank gap, and remembers the last + // dragged width for the rest of this window's lifetime (not persisted beyond it). + private void UpdateQueueColumn() + { + var queueColumn = BodyGrid.ColumnDefinitions[2]; + if (_vm?.HasQueued == true) + { + queueColumn.MinWidth = MinQueueWidth; + queueColumn.Width = _queueWidth; + } + else + { + queueColumn.MinWidth = 0; + queueColumn.Width = new GridLength(0, GridUnitType.Pixel); + } + } + + // Replaces the old UniformGrid with a real Grid + GridSplitters so panes resize + // individually. Column splitters span every row (resize that whole column vs. its + // neighbor); row splitters span every column. Rebuilt from scratch whenever the pane + // count or column count changes — per-pane sizes are intentionally not preserved across + // a rebuild (session-only resizing, reset to the default split when panes come/go). + private void RebuildOverviewGrid() + { + OverviewGrid.Children.Clear(); + OverviewGrid.ColumnDefinitions.Clear(); + OverviewGrid.RowDefinitions.Clear(); + + var panes = _vm?.Panes; + if (panes is not { Count: > 0 }) return; + + var columns = Math.Max(1, _vm!.ColumnCount); + var rows = (int)Math.Ceiling(panes.Count / (double)columns); + + for (var c = 0; c < columns; c++) + { + if (c > 0) OverviewGrid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto)); + OverviewGrid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star)) { MinWidth = MinPaneWidth }); + } + + for (var r = 0; r < rows; r++) + { + if (r > 0) OverviewGrid.RowDefinitions.Add(new RowDefinition(GridLength.Auto)); + OverviewGrid.RowDefinitions.Add(new RowDefinition(new GridLength(1, GridUnitType.Star)) { MinHeight = MinPaneHeight }); + } + + var totalGridRows = rows * 2 - 1; + var totalGridCols = columns * 2 - 1; + + for (var c = 1; c < columns; c++) + { + var splitter = new GridSplitter + { + Width = SplitterSize, + Background = Brushes.Transparent, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + ResizeDirection = GridResizeDirection.Columns, + ResizeBehavior = GridResizeBehavior.PreviousAndNext, + }; + Grid.SetColumn(splitter, c * 2 - 1); + Grid.SetRow(splitter, 0); + Grid.SetRowSpan(splitter, totalGridRows); + OverviewGrid.Children.Add(splitter); + } + + for (var r = 1; r < rows; r++) + { + var splitter = new GridSplitter + { + Height = SplitterSize, + Background = Brushes.Transparent, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + ResizeDirection = GridResizeDirection.Rows, + ResizeBehavior = GridResizeBehavior.PreviousAndNext, + }; + Grid.SetColumn(splitter, 0); + Grid.SetColumnSpan(splitter, totalGridCols); + Grid.SetRow(splitter, r * 2 - 1); + OverviewGrid.Children.Add(splitter); + } + + for (var i = 0; i < panes.Count; i++) + { + var presenter = new ContentControl { Content = panes[i] }; + Grid.SetColumn(presenter, (i % columns) * 2); + Grid.SetRow(presenter, (i / columns) * 2); + OverviewGrid.Children.Add(presenter); + } + } }