feat(interactive): host task-based ConPTY sessions in Command Center
Adds an 'Open ConPTY session' entry that fetches a task's launch spec and hosts an embedded ConPTY terminal as a Mission Control pane, coexisting with the streamed-log monitor panes (streaming stack untouched). Introduces IMissionControlPane + ConPtyPaneViewModel, a non-destructive Panes mirror (Monitors prefix + ConPtySessions suffix) so unrelated monitor churn never tears down a live terminal, and a grid<->tabs layout toggle. Launch failures surface via the footer error strip.
This commit is contained in:
@@ -7,11 +7,12 @@ using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Ui.Helpers;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.MissionControl;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Islands;
|
||||
|
||||
public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
||||
public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionControlPane, IDisposable
|
||||
{
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private readonly IWorkerClient _worker;
|
||||
|
||||
@@ -819,6 +819,18 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
catch (Exception ex) { ErrorReported?.Invoke(Loc.T("vm.tasksIsland.runInteractiveFailed", ex.Message)); }
|
||||
}
|
||||
|
||||
// Distinct from RunInteractivelyAsync (the old in-app streaming session) — this opens the
|
||||
// task in an embedded ConPTY terminal pane in the Command Center. The shell owns the
|
||||
// Mission Control view model, so this just raises an event for it to act on.
|
||||
public event Action<string>? OpenConPtySessionRequested;
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenConPtySession(TaskRowViewModel? row)
|
||||
{
|
||||
if (row is null) return;
|
||||
OpenConPtySessionRequested?.Invoke(row.Id);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task PickUpInTerminalAsync(TaskRowViewModel? row)
|
||||
{
|
||||
|
||||
@@ -215,6 +215,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
MissionControl.OpenInApp = id => _ = RevealTaskAsync(id);
|
||||
MissionControl.ShowDetached = (monitor, reDock) => Dialogs?.ShowDetachedMonitor(monitor, reDock);
|
||||
MissionControl.OpenSettingsRequested = () => Lists.OpenSettingsCommand.Execute(null);
|
||||
MissionControl.ErrorReported += FlashFooterError;
|
||||
_updateCheck = updateCheck;
|
||||
_installerLocator = installerLocator;
|
||||
_workerLocator = workerLocator;
|
||||
@@ -228,6 +229,11 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
Tasks.NotesRequested += () => Details.ShowNotes();
|
||||
Tasks.PrepRequested += () => Details.ShowPrep();
|
||||
Tasks.ErrorReported += FlashFooterError;
|
||||
Tasks.OpenConPtySessionRequested += taskId =>
|
||||
{
|
||||
OpenMissionControl();
|
||||
_ = MissionControl.OpenConPtySessionAsync(taskId);
|
||||
};
|
||||
Tasks.TasksChanged += (_, _) => _ = Lists.RefreshCountsAsync();
|
||||
Tasks.OpenListSettingsRequested += (_, _) =>
|
||||
{
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ClaudeDo.Ui.Services;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.MissionControl;
|
||||
|
||||
/// <summary>
|
||||
/// Command Center pane hosting an embedded ConPTY terminal for one task's interactive Claude
|
||||
/// session (task-based only — no ad-hoc/free sessions yet). Distinct from the streamed-log
|
||||
/// <see cref="ClaudeDo.Ui.ViewModels.Islands.TaskMonitorViewModel"/> pane; the two coexist until
|
||||
/// the streaming interactive stack is removed.
|
||||
/// </summary>
|
||||
public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControlPane, IDisposable
|
||||
{
|
||||
public string TaskId { get; }
|
||||
|
||||
[ObservableProperty] private string _displayTitle;
|
||||
|
||||
public InteractiveTerminalViewModel Terminal { get; } = new();
|
||||
|
||||
/// <summary>Raised when the terminal failed to start — the host surfaces this via the footer error strip.</summary>
|
||||
public event Action<string>? ErrorReported;
|
||||
|
||||
/// <summary>Set by the host (Mission Control) to remove this pane from its collection.</summary>
|
||||
public Action<ConPtyPaneViewModel>? CloseRequested { get; set; }
|
||||
|
||||
public ConPtyPaneViewModel(string taskId, string displayTitle, TerminalLaunchDescriptor descriptor)
|
||||
{
|
||||
TaskId = taskId;
|
||||
_displayTitle = displayTitle;
|
||||
Terminal.PropertyChanged += OnTerminalPropertyChanged;
|
||||
Terminal.Start(descriptor);
|
||||
}
|
||||
|
||||
private void OnTerminalPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(InteractiveTerminalViewModel.StartError) && Terminal.StartError is { Length: > 0 } error)
|
||||
ErrorReported?.Invoke(error);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Close() => CloseRequested?.Invoke(this);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Terminal.PropertyChanged -= OnTerminalPropertyChanged;
|
||||
Terminal.Kill();
|
||||
Terminal.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ClaudeDo.Ui.ViewModels.MissionControl;
|
||||
|
||||
/// <summary>
|
||||
/// Common contract for anything hosted as a pane in the Command Center — implemented by both
|
||||
/// the streamed-log <see cref="ClaudeDo.Ui.ViewModels.Islands.TaskMonitorViewModel"/> and the
|
||||
/// embedded ConPTY <see cref="ConPtyPaneViewModel"/> — so the layout toggle (grid/tabs) can
|
||||
/// bind one heterogeneous pane collection.
|
||||
/// </summary>
|
||||
public interface IMissionControlPane
|
||||
{
|
||||
string DisplayTitle { get; }
|
||||
}
|
||||
@@ -5,8 +5,10 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
using ClaudeDo.Ui.ViewModels.MissionControl;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels;
|
||||
@@ -23,8 +25,28 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
|
||||
public ObservableCollection<TaskMonitorViewModel> Monitors { get; } = new();
|
||||
|
||||
// Embedded ConPTY sessions (task-based only) — a manual cockpit detached from the
|
||||
// review/merge/status machinery. Mirrored into Panes alongside the streamed-log Monitors.
|
||||
public ObservableCollection<ConPtyPaneViewModel> ConPtySessions { get; } = new();
|
||||
|
||||
// Unified view of Monitors ++ ConPtySessions (in that order) so the layout toggle can
|
||||
// present one heterogeneous collection as either a grid or tabs.
|
||||
public ObservableCollection<IMissionControlPane> Panes { get; } = new();
|
||||
|
||||
[ObservableProperty] private int _columnCount = 1;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(LayoutToggleLabel))]
|
||||
private bool _isFocusMode;
|
||||
|
||||
[ObservableProperty] private IMissionControlPane? _focusedPane;
|
||||
|
||||
public string LayoutToggleLabel => Loc.T(IsFocusMode ? "missionControl.overviewMode" : "missionControl.focusMode");
|
||||
|
||||
/// <summary>Surfaces a Command Center failure (e.g. a ConPTY launch spec fetch) — the shell
|
||||
/// wires this into the footer error strip, same as the island view models' ErrorReported.</summary>
|
||||
public event Action<string>? ErrorReported;
|
||||
|
||||
private Action<string>? _openInApp;
|
||||
public Action<string>? OpenInApp
|
||||
{
|
||||
@@ -44,6 +66,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
public Action? OpenSettingsRequested { get; set; }
|
||||
|
||||
public bool HasMonitors => Monitors.Count > 0;
|
||||
public bool HasPanes => Panes.Count > 0;
|
||||
|
||||
// Read-only view of the worker queue (tasks waiting to run), shown as a side strip.
|
||||
public ObservableCollection<QueuedTaskViewModel> Queued { get; } = new();
|
||||
@@ -55,6 +78,8 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
_worker = worker;
|
||||
|
||||
Monitors.CollectionChanged += OnMonitorsChanged;
|
||||
ConPtySessions.CollectionChanged += OnConPtySessionsChanged;
|
||||
Panes.CollectionChanged += OnPanesChanged;
|
||||
|
||||
_onTaskStarted = (slot, taskId, startedAt) => { EnsureMonitor(taskId); _ = RefreshQueueAsync(); };
|
||||
_worker.TaskStartedEvent += _onTaskStarted;
|
||||
@@ -192,6 +217,9 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
[RelayCommand]
|
||||
private void OpenSettings() => OpenSettingsRequested?.Invoke();
|
||||
|
||||
[RelayCommand]
|
||||
private void ToggleLayout() => IsFocusMode = !IsFocusMode;
|
||||
|
||||
public void MoveMonitor(TaskMonitorViewModel dragged, TaskMonitorViewModel target)
|
||||
{
|
||||
if (ReferenceEquals(dragged, target)) return;
|
||||
@@ -201,15 +229,106 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
Monitors.Move(from, to);
|
||||
}
|
||||
|
||||
// Fetches the launch spec for a task's worktree and hosts an embedded ConPTY session as a
|
||||
// Command Center pane (task-based only). A distinct entry point from RunInteractivelyAsync's
|
||||
// streaming session — the two coexist until the streaming stack is removed.
|
||||
public async System.Threading.Tasks.Task OpenConPtySessionAsync(string taskId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(taskId)) return;
|
||||
if (ConPtySessions.FirstOrDefault(s => s.TaskId == taskId) is { } existing)
|
||||
{
|
||||
FocusedPane = existing;
|
||||
return;
|
||||
}
|
||||
|
||||
var title = taskId;
|
||||
try
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||
var entity = await ctx.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId);
|
||||
if (entity?.Title is { Length: > 0 } t) title = t;
|
||||
}
|
||||
catch { /* best-effort title lookup */ }
|
||||
|
||||
try
|
||||
{
|
||||
var spec = await _worker.GetInteractiveLaunchSpecAsync(taskId);
|
||||
var descriptor = new TerminalLaunchDescriptor(spec.Cwd, spec.Exe, spec.Args, spec.Env);
|
||||
var pane = new ConPtyPaneViewModel(taskId, title, descriptor);
|
||||
pane.ErrorReported += OnConPtyPaneError;
|
||||
pane.CloseRequested += CloseConPtySession;
|
||||
ConPtySessions.Add(pane);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorReported?.Invoke(Loc.T("missionControl.conptyLaunchFailed", ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConPtyPaneError(string message) => ErrorReported?.Invoke(message);
|
||||
|
||||
private void CloseConPtySession(ConPtyPaneViewModel pane)
|
||||
{
|
||||
if (!ConPtySessions.Contains(pane)) return;
|
||||
pane.ErrorReported -= OnConPtyPaneError;
|
||||
pane.CloseRequested -= CloseConPtySession;
|
||||
ConPtySessions.Remove(pane);
|
||||
pane.Dispose();
|
||||
}
|
||||
|
||||
// Mirrors Monitors' add/remove/move into the front (Monitors-prefix) section of Panes.
|
||||
private void OnMonitorsChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
ColumnCount = Monitors.Count switch
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
Panes.Insert(e.NewStartingIndex, (TaskMonitorViewModel)e.NewItems![0]!);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
Panes.RemoveAt(e.OldStartingIndex);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
Panes.Move(e.OldStartingIndex, e.NewStartingIndex);
|
||||
break;
|
||||
default: // Reset (Dispose's Monitors.Clear())
|
||||
foreach (var p in Panes.OfType<TaskMonitorViewModel>().ToList())
|
||||
Panes.Remove(p);
|
||||
break;
|
||||
}
|
||||
OnPropertyChanged(nameof(HasMonitors));
|
||||
}
|
||||
|
||||
// Mirrors ConPtySessions' add/remove into the tail (ConPtySessions-suffix) section of Panes.
|
||||
private void OnConPtySessionsChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
Panes.Insert(Monitors.Count + e.NewStartingIndex, (ConPtyPaneViewModel)e.NewItems![0]!);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
Panes.RemoveAt(Monitors.Count + e.OldStartingIndex);
|
||||
break;
|
||||
default: // Reset
|
||||
foreach (var p in Panes.OfType<ConPtyPaneViewModel>().ToList())
|
||||
Panes.Remove(p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPanesChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
ColumnCount = Panes.Count switch
|
||||
{
|
||||
<= 1 => 1,
|
||||
<= 4 => 2,
|
||||
_ => 3,
|
||||
};
|
||||
OnPropertyChanged(nameof(HasMonitors));
|
||||
OnPropertyChanged(nameof(HasPanes));
|
||||
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems?[0] is IMissionControlPane added)
|
||||
FocusedPane = added;
|
||||
else if (e.Action == NotifyCollectionChangedAction.Remove && ReferenceEquals(FocusedPane, e.OldItems?[0]))
|
||||
FocusedPane = Panes.LastOrDefault();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -220,8 +339,18 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
_worker.ConnectionRestoredEvent -= _onConnectionRestored;
|
||||
_worker.InteractiveSessionStartedEvent -= _onInteractiveStarted;
|
||||
Monitors.CollectionChanged -= OnMonitorsChanged;
|
||||
ConPtySessions.CollectionChanged -= OnConPtySessionsChanged;
|
||||
Panes.CollectionChanged -= OnPanesChanged;
|
||||
foreach (var m in Monitors) m.Dispose();
|
||||
Monitors.Clear();
|
||||
foreach (var c in ConPtySessions.ToList())
|
||||
{
|
||||
c.ErrorReported -= OnConPtyPaneError;
|
||||
c.CloseRequested -= CloseConPtySession;
|
||||
c.Dispose();
|
||||
}
|
||||
ConPtySessions.Clear();
|
||||
Panes.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
<Separator/>
|
||||
<MenuItem Header="{loc:Tr tasks.ctxRunInteractively}"
|
||||
Click="OnRunInteractivelyClick"/>
|
||||
<MenuItem Header="{loc:Tr tasks.ctxOpenConPtySession}"
|
||||
Click="OnOpenConPtySessionClick"/>
|
||||
<MenuItem Header="{loc:Tr tasks.ctxPickUpInTerminal}"
|
||||
Click="OnPickUpInTerminalClick"
|
||||
IsVisible="{Binding CanPickUpInTerminal}"/>
|
||||
|
||||
@@ -67,6 +67,12 @@ public partial class TaskRowView : UserControl
|
||||
await vm.RunInteractivelyCommand.ExecuteAsync(row);
|
||||
}
|
||||
|
||||
private void OnOpenConPtySessionClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
||||
vm.OpenConPtySessionCommand.Execute(row);
|
||||
}
|
||||
|
||||
private async void OnPickUpInTerminalClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels.MissionControl"
|
||||
xmlns:views="using:ClaudeDo.Ui.Views"
|
||||
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
||||
xmlns:conv="using:Avalonia.Data.Converters"
|
||||
x:DataType="vm:ConPtyPaneViewModel"
|
||||
x:Class="ClaudeDo.Ui.Views.MissionControl.ConPtyPaneView">
|
||||
<Border Classes="monitor-pane" BorderThickness="1" CornerRadius="10" ClipToBounds="True">
|
||||
<DockPanel LastChildFill="True">
|
||||
|
||||
<!-- Header: title + close -->
|
||||
<Border DockPanel.Dock="Top"
|
||||
Background="{DynamicResource Surface2Brush}"
|
||||
BorderBrush="{DynamicResource LineBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="8,3">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="meta" Text="{Binding DisplayTitle}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
ToolTip.Tip="{Binding DisplayTitle}"
|
||||
Foreground="{DynamicResource TextDimBrush}"
|
||||
VerticalAlignment="Center" Margin="4,0,0,0" />
|
||||
<Button Grid.Column="1" Classes="title-ctrl"
|
||||
Command="{Binding CloseCommand}"
|
||||
ToolTip.Tip="{loc:Tr missionControl.closeSession}">
|
||||
<PathIcon Data="{StaticResource Icon.WinClose}" Width="12" Height="12"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Start-failure banner -->
|
||||
<Border DockPanel.Dock="Top"
|
||||
IsVisible="{Binding Terminal.StartError, Converter={x:Static conv:ObjectConverters.IsNotNull}}"
|
||||
Background="{DynamicResource ErrorTintBrush}"
|
||||
BorderBrush="{DynamicResource BloodBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="12,6">
|
||||
<TextBlock Classes="meta" Text="{Binding Terminal.StartError}"
|
||||
Foreground="{DynamicResource BloodBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
|
||||
<!-- Embedded ConPTY terminal -->
|
||||
<views:InteractiveTerminalView DataContext="{Binding Terminal}" />
|
||||
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,8 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace ClaudeDo.Ui.Views.MissionControl;
|
||||
|
||||
public partial class ConPtyPaneView : UserControl
|
||||
{
|
||||
public ConPtyPaneView() => InitializeComponent();
|
||||
}
|
||||
@@ -2,10 +2,21 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
|
||||
xmlns:vmi="using:ClaudeDo.Ui.ViewModels.Islands"
|
||||
xmlns:vmm="using:ClaudeDo.Ui.ViewModels.MissionControl"
|
||||
xmlns:mc="using:ClaudeDo.Ui.Views.MissionControl"
|
||||
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
||||
x:DataType="vm:MissionControlViewModel"
|
||||
x:Class="ClaudeDo.Ui.Views.MissionControl.MissionControlView">
|
||||
<UserControl.DataTemplates>
|
||||
<!-- Polymorphic pane templates: both the grid ItemsControl and the focus-mode TabControl
|
||||
resolve per-item content through these (no explicit ItemTemplate on either). -->
|
||||
<DataTemplate DataType="vmi:TaskMonitorViewModel">
|
||||
<mc:MonitorPaneView Margin="6" />
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="vmm:ConPtyPaneViewModel">
|
||||
<mc:ConPtyPaneView Margin="6" />
|
||||
</DataTemplate>
|
||||
</UserControl.DataTemplates>
|
||||
<DockPanel LastChildFill="True" Background="{DynamicResource VoidBrush}"
|
||||
DragDrop.AllowDrop="True">
|
||||
|
||||
@@ -21,6 +32,12 @@
|
||||
LetterSpacing="1.4" VerticalAlignment="Center" />
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<Button Classes="icon-btn"
|
||||
Command="{Binding ToggleLayoutCommand}"
|
||||
ToolTip.Tip="{Binding LayoutToggleLabel}">
|
||||
<PathIcon Data="{StaticResource Icon.Grid}" Width="15" Height="15"
|
||||
Foreground="{DynamicResource TextMuteBrush}"/>
|
||||
</Button>
|
||||
<Button Classes="icon-btn"
|
||||
Command="{Binding OpenSettingsCommand}"
|
||||
ToolTip.Tip="{loc:Tr missionControl.settings}">
|
||||
@@ -72,22 +89,29 @@
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Grid / empty state -->
|
||||
<!-- Grid / tabs / empty state -->
|
||||
<Panel Margin="6">
|
||||
<ItemsControl ItemsSource="{Binding Monitors}" IsVisible="{Binding HasMonitors}">
|
||||
<!-- Overview mode: the existing UniformGrid, one tile per pane -->
|
||||
<ItemsControl ItemsSource="{Binding Panes}" IsVisible="{Binding !IsFocusMode}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid x:CompileBindings="False" Columns="{Binding ColumnCount}" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vmi:TaskMonitorViewModel">
|
||||
<mc:MonitorPaneView Margin="6" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock IsVisible="{Binding !HasMonitors}"
|
||||
<!-- Focus mode: one pane large, switched via tabs -->
|
||||
<TabControl ItemsSource="{Binding Panes}"
|
||||
SelectedItem="{Binding FocusedPane}"
|
||||
IsVisible="{Binding IsFocusMode}">
|
||||
<TabControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vmm:IMissionControlPane">
|
||||
<TextBlock Text="{Binding DisplayTitle}" TextTrimming="CharacterEllipsis" MaxWidth="160" />
|
||||
</DataTemplate>
|
||||
</TabControl.ItemTemplate>
|
||||
</TabControl>
|
||||
|
||||
<TextBlock IsVisible="{Binding !HasPanes}"
|
||||
Text="{loc:Tr missionControl.empty}"
|
||||
Foreground="{DynamicResource TextMuteBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
|
||||
Reference in New Issue
Block a user