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; 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. private async void OnNewSessionClicked(object? sender, RoutedEventArgs e) { if (DataContext is not MissionControlViewModel vm) return; var topLevel = TopLevel.GetTopLevel(this); if (topLevel is null) return; var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = "Choose a directory", AllowMultiple = false, }); if (folders.Count == 0) return; 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); } } }