Merge claudedo/02777289600f499ab274e844eb070ff4

This commit is contained in:
mika kuns
2026-08-05 09:24:21 +02:00
19 changed files with 20 additions and 608 deletions
@@ -1,8 +1,6 @@
using System;
using System.Threading.Tasks;
using ClaudeDo.Ui.ViewModels;
using ClaudeDo.Ui.ViewModels.Conflicts;
using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.ViewModels.Modals;
namespace ClaudeDo.Ui.Services;
@@ -37,7 +35,4 @@ public interface IDialogService
/// <summary>Show (or re-show + focus) the modeless Mission Control window. Lazily created; hides on close.</summary>
void ShowMissionControl(MissionControlViewModel vm);
/// <summary>Show a detached monitor in its own window; <paramref name="onClosed"/> re-docks it when that window closes.</summary>
void ShowDetachedMonitor(TaskMonitorViewModel monitor, Action onClosed);
}
@@ -8,12 +8,11 @@ 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, IMissionControlPane, IDisposable
public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
{
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private readonly IWorkerClient _worker;
@@ -28,13 +27,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionContro
[ObservableProperty] private string _agentState = "idle";
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DisplayTitle))]
private string? _title;
public string DisplayTitle =>
string.IsNullOrWhiteSpace(Title) ? (SubscribedTaskId ?? "task") : Title!;
public string AgentStatusLabel => Loc.T($"vm.agentStatus.{AgentState}");
public bool IsIdle => AgentState == "idle";
public bool IsQueued => AgentState == "queued";
@@ -205,43 +197,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionContro
ClearPendingQuestion();
}
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DetachTooltip))]
private bool _isDetached;
// Localized tooltip for the detach/re-dock toggle button.
public string DetachTooltip => Loc.T(IsDetached ? "missionControl.redock" : "missionControl.detach");
// Set by the detached window so the re-dock action can close it.
public Action? CloseWindowRequested { get; set; }
// Set by the host (e.g. Mission Control) to navigate the main app to this task.
public Action<string>? OpenInAppRequested { get; set; }
// Set by the host (Mission Control) to pop this monitor out into its own window.
public Action<TaskMonitorViewModel>? DetachRequested { get; set; }
[RelayCommand]
private void Detach()
{
if (IsDetached) CloseWindowRequested?.Invoke(); // re-dock: close the detached window
else DetachRequested?.Invoke(this); // detach: pop out to its own window
}
[RelayCommand]
private void OpenInApp()
{
if (!string.IsNullOrEmpty(_subscribedTaskId))
OpenInAppRequested?.Invoke(_subscribedTaskId);
}
[RelayCommand]
private async System.Threading.Tasks.Task CancelTask()
{
if (!string.IsNullOrEmpty(_subscribedTaskId) && (IsRunning || IsQueued))
await _worker.CancelTaskAsync(_subscribedTaskId);
}
public void SetTaskId(string id) => _subscribedTaskId = id;
public void ApplyState(ClaudeDo.Data.Models.TaskStatus status) =>
@@ -213,7 +213,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
Lists = lists; Tasks = tasks; Details = details; Worker = worker;
MissionControl = missionControl;
MissionControl.OpenInApp = id => _ = RevealTaskAsync(id);
MissionControl.ShowDetached = (monitor, reDock) => Dialogs?.ShowDetachedMonitor(monitor, reDock);
MissionControl.OpenSettingsRequested = () => Lists.OpenSettingsCommand.Execute(null);
MissionControl.ErrorReported += FlashFooterError;
// Keep the task rows' "Interactive" chip in step with Mission Control's open ConPTY panes.
@@ -9,9 +9,7 @@ namespace ClaudeDo.Ui.ViewModels.MissionControl;
/// <summary>
/// Command Center pane hosting an embedded ConPTY terminal for either one task's interactive
/// Claude session, or an ad-hoc/free session in a user-chosen directory (no task, <see cref="TaskId"/>
/// is null — ad-hoc panes are never deduped, unlike task-based ones). Distinct from the streamed-log
/// <see cref="ClaudeDo.Ui.ViewModels.Islands.TaskMonitorViewModel"/> pane; the two coexist until
/// the streaming interactive stack is removed.
/// is null — ad-hoc panes are never deduped, unlike task-based ones).
/// </summary>
public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControlPane, IDisposable
{
@@ -1,10 +1,9 @@
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.
/// Common contract for anything hosted as a pane in the Command Center — currently only
/// <see cref="ConPtyPaneViewModel"/>, kept as its own abstraction so the grid/tabs layout
/// toggle binds a pane collection rather than a concrete ConPTY-specific type.
/// </summary>
public interface IMissionControlPane
{
@@ -5,10 +5,8 @@ using System.Linq;
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;
@@ -23,14 +21,12 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
private readonly Action<string> _onTaskUpdated;
private readonly Action _onConnectionRestored;
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.
// review/merge/status machinery.
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.
// Mirror of ConPtySessions typed as the pane abstraction so the layout toggle (grid/tabs)
// binds one contract rather than a ConPTY-specific type.
public ObservableCollection<IMissionControlPane> Panes { get; } = new();
[ObservableProperty] private int _columnCount = 1;
@@ -47,25 +43,11 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
/// 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
{
get => _openInApp;
set
{
_openInApp = value;
foreach (var m in Monitors) m.OpenInAppRequested = value;
}
}
// View-layer seam: show a detached monitor in its own window. Second arg is the re-dock callback
// invoked when that window closes.
public Action<TaskMonitorViewModel, Action>? ShowDetached { get; set; }
public Action<string>? OpenInApp { get; set; }
// View-layer seam: open the app Settings modal from the Mission Control window.
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.
@@ -77,7 +59,6 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
_dbFactory = dbFactory;
_worker = worker;
Monitors.CollectionChanged += OnMonitorsChanged;
ConPtySessions.CollectionChanged += OnConPtySessionsChanged;
Panes.CollectionChanged += OnPanesChanged;
@@ -119,7 +100,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
Title = r.Title ?? string.Empty,
IsBlocked = r.BlockedByTaskId != null,
IsRunning = r.Status == ClaudeDo.Data.Models.TaskStatus.Running,
OpenInAppCommand = new RelayCommand(() => _openInApp?.Invoke(id)),
OpenInAppCommand = new RelayCommand(() => OpenInApp?.Invoke(id)),
});
}
OnPropertyChanged(nameof(HasQueued));
@@ -147,88 +128,12 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
await RefreshQueueAsync();
}
// Kept intentionally — unused since auto-seeding was disabled (2026-07-29).
internal void SeedActive()
{
foreach (var a in _worker.GetActiveTasks())
EnsureMonitor(a.TaskId);
}
// Kept intentionally — unused since auto-seeding was disabled (2026-07-29).
internal void EnsureMonitor(string taskId)
{
if (string.IsNullOrEmpty(taskId)) return;
if (Monitors.Any(m => m.SubscribedTaskId == taskId)) return;
var monitor = new TaskMonitorViewModel(_dbFactory, _worker);
monitor.SetTaskId(taskId);
monitor.OpenInAppRequested = _openInApp;
monitor.DetachRequested = Detach;
Monitors.Add(monitor);
_ = HydrateAsync(monitor, taskId);
}
private void Detach(TaskMonitorViewModel monitor)
{
if (!Monitors.Contains(monitor)) return;
monitor.IsDetached = true;
Monitors.Remove(monitor); // drop from grid — do NOT dispose; it keeps streaming
ShowDetached?.Invoke(monitor, () => ReDock(monitor));
}
private void ReDock(TaskMonitorViewModel monitor)
{
monitor.IsDetached = false;
if (!Monitors.Contains(monitor) && monitor.SubscribedTaskId is not null)
Monitors.Add(monitor); // back into the grid
}
private async System.Threading.Tasks.Task HydrateAsync(TaskMonitorViewModel monitor, string taskId)
{
try
{
await using var ctx = await _dbFactory.CreateDbContextAsync();
var entity = await ctx.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId);
if (entity is null || monitor.SubscribedTaskId != taskId) return;
monitor.ApplyState(entity.Status);
monitor.Title = entity.Title;
var latestRun = await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId);
monitor.ApplyOutcome(entity.Result, latestRun?.ErrorMarkdown);
await monitor.ReplayLogFileAsync(entity.LogPath, CancellationToken.None);
// Re-attach: if the task is blocked on an AskUser question right now, surface it.
var pending = await _worker.GetPendingQuestionAsync(taskId);
if (pending is not null && monitor.SubscribedTaskId == taskId)
monitor.SetPendingQuestion(pending.QuestionId, pending.Question);
}
catch { /* best-effort hydrate */ }
}
[RelayCommand]
private void ClearFinished()
{
foreach (var m in Monitors.Where(m => m.IsDone || m.IsFailed || m.IsCancelled || m.IsWaitingForReview).ToList())
{
Monitors.Remove(m);
m.Dispose();
}
}
[RelayCommand]
private void OpenSettings() => OpenSettingsRequested?.Invoke();
[RelayCommand]
private void ToggleLayout() => IsFocusMode = !IsFocusMode;
public void MoveMonitor(TaskMonitorViewModel dragged, TaskMonitorViewModel target)
{
if (ReferenceEquals(dragged, target)) return;
var from = Monitors.IndexOf(dragged);
var to = Monitors.IndexOf(target);
if (from < 0 || to < 0) return;
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).
public async System.Threading.Tasks.Task OpenConPtySessionAsync(string taskId)
@@ -381,42 +286,19 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
pane.Dispose();
}
// Mirrors Monitors' add/remove/move into the front (Monitors-prefix) section of Panes.
private void OnMonitorsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
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.
// Mirrors ConPtySessions' add/remove 1:1 into Panes (typed as the pane abstraction).
private void OnConPtySessionsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Panes.Insert(Monitors.Count + e.NewStartingIndex, (ConPtyPaneViewModel)e.NewItems![0]!);
Panes.Insert(e.NewStartingIndex, (ConPtyPaneViewModel)e.NewItems![0]!);
break;
case NotifyCollectionChangedAction.Remove:
Panes.RemoveAt(Monitors.Count + e.OldStartingIndex);
Panes.RemoveAt(e.OldStartingIndex);
break;
default: // Reset
foreach (var p in Panes.OfType<ConPtyPaneViewModel>().ToList())
Panes.Remove(p);
Panes.Clear();
break;
}
}
@@ -442,11 +324,8 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
_worker.TaskFinishedEvent -= _onTaskFinished;
_worker.TaskUpdatedEvent -= _onTaskUpdated;
_worker.ConnectionRestoredEvent -= _onConnectionRestored;
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;
@@ -81,8 +81,7 @@
</StackPanel>
</Border>
<!-- Question prompt (AskUser): answer the running task's question inline, mirroring
the Mission Control banner. State is shared via the same TaskMonitorViewModel. -->
<!-- Question prompt (AskUser): answer the running task's question inline. -->
<Border DockPanel.Dock="Top"
IsVisible="{Binding Monitor.HasPendingQuestion}"
Margin="0,0,0,10" Padding="12,8"
@@ -1,24 +1,17 @@
<UserControl xmlns="https://github.com/avaloniaui"
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">
<DockPanel LastChildFill="True" Background="{DynamicResource VoidBrush}">
<!-- Header -->
<Border DockPanel.Dock="Top"
@@ -50,9 +43,6 @@
<PathIcon Data="{StaticResource Icon.Settings}" Width="15" Height="15"
Foreground="{DynamicResource TextMuteBrush}"/>
</Button>
<Button Classes="btn"
Content="{loc:Tr missionControl.clearFinished}"
Command="{Binding ClearFinishedCommand}" />
</StackPanel>
</Grid>
</Border>
@@ -1,25 +1,15 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using Avalonia.VisualTree;
using ClaudeDo.Ui.ViewModels;
using ClaudeDo.Ui.ViewModels.Islands;
namespace ClaudeDo.Ui.Views.MissionControl;
public partial class MissionControlView : UserControl
{
// Shared with MonitorPaneView (the drag source).
public static readonly DataFormat<string> PaneFormat =
DataFormat.CreateStringApplicationFormat("claudedo-monitor-pane");
public MissionControlView()
{
InitializeComponent();
AddHandler(DragDrop.DragOverEvent, OnPaneDragOver);
AddHandler(DragDrop.DropEvent, OnPaneDrop);
}
// Ad-hoc ConPTY session: the view owns the folder picker, the VM only takes the chosen path.
@@ -38,31 +28,4 @@ public partial class MissionControlView : UserControl
await vm.OpenAdHocConPtySessionAsync(folders[0].Path.LocalPath);
}
private void OnPaneDragOver(object? sender, DragEventArgs e)
{
var dt = e.DataTransfer;
e.DragEffects = (dt?.Contains(PaneFormat) ?? false) ? DragDropEffects.Move : DragDropEffects.None;
}
private void OnPaneDrop(object? sender, DragEventArgs e)
{
if (DataContext is not MissionControlViewModel vm) return;
var dt = e.DataTransfer;
if (dt is null) return;
// A pane dragged within the grid → reorder. (Drag-to-queue from the main app now arrives
// via the custom ghost drag's screen hit-test, not an OLE drop.)
var draggedId = dt.TryGetValue(PaneFormat);
if (string.IsNullOrEmpty(draggedId)) return;
if (e.Source is not Avalonia.Visual src) return;
var targetPane = src.FindAncestorOfType<MonitorPaneView>();
if (targetPane?.DataContext is not TaskMonitorViewModel target) return;
var dragged = vm.Monitors.FirstOrDefault(m => m.SubscribedTaskId == draggedId);
if (dragged is null) return;
vm.MoveMonitor(dragged, target);
}
}
@@ -1,114 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:DataType="vm:TaskMonitorViewModel"
x:Class="ClaudeDo.Ui.Views.MissionControl.MonitorPaneView">
<Border Classes="monitor-pane"
DragDrop.AllowDrop="True"
Classes.mon-review="{Binding IsWaitingForReview}"
Classes.mon-done="{Binding IsDone}"
Classes.mon-roadblock="{Binding HasRoadblock}"
Classes.mon-failed="{Binding IsFailed}"
BorderThickness="1" CornerRadius="10" ClipToBounds="True">
<DockPanel LastChildFill="True">
<!-- Header: per-task action icons -->
<Border DockPanel.Dock="Top"
Background="{DynamicResource Surface2Brush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1" Padding="6,3"
PointerPressed="OnHeaderPressed">
<StackPanel Orientation="Horizontal" Spacing="2"
HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Classes="title-ctrl"
Command="{Binding OpenInAppCommand}"
ToolTip.Tip="{loc:Tr missionControl.openInApp}">
<PathIcon Data="{StaticResource Icon.ArrowOut}" Width="12" Height="12"/>
</Button>
<Button Classes="title-ctrl"
Command="{Binding DetachCommand}"
ToolTip.Tip="{Binding DetachTooltip}">
<PathIcon Data="{StaticResource Icon.WinMax}" Width="12" Height="12"/>
</Button>
<Button Classes="title-ctrl"
Command="{Binding CancelTaskCommand}"
IsVisible="{Binding IsRunning}"
ToolTip.Tip="{loc:Tr missionControl.cancel}">
<PathIcon Data="{StaticResource Icon.WinClose}" Width="12" Height="12"/>
</Button>
</StackPanel>
</Border>
<!-- Roadblock banner (amber) -->
<Border DockPanel.Dock="Top"
IsVisible="{Binding HasRoadblock}"
Background="{DynamicResource RoadblockTintBrush}"
BorderBrush="{DynamicResource AmberBrush}"
BorderThickness="0,0,0,1" Padding="12,6">
<StackPanel Orientation="Horizontal" Spacing="8">
<PathIcon Data="{StaticResource Icon.Warning}"
Foreground="{DynamicResource AmberBrush}"
Width="13" Height="13" VerticalAlignment="Center" />
<TextBlock Classes="meta" Text="{Binding Roadblocks}"
Foreground="{DynamicResource AmberBrush}"
TextWrapping="Wrap" VerticalAlignment="Center" />
</StackPanel>
</Border>
<!-- Failure banner (red) -->
<Border DockPanel.Dock="Top"
IsVisible="{Binding ShowRoadblock}"
Background="{DynamicResource ErrorTintBrush}"
BorderBrush="{DynamicResource BloodBrush}"
BorderThickness="0,0,0,1" Padding="12,6">
<StackPanel Orientation="Horizontal" Spacing="8">
<PathIcon Data="{StaticResource Icon.Warning}"
Foreground="{DynamicResource BloodBrush}"
Width="13" Height="13" VerticalAlignment="Center" />
<TextBlock Classes="meta" Text="{Binding RoadblockMessage}"
Foreground="{DynamicResource BloodBrush}"
TextWrapping="Wrap" VerticalAlignment="Center" />
</StackPanel>
</Border>
<!-- Question prompt (AskUser): answer a running task's question inline -->
<Border DockPanel.Dock="Top"
IsVisible="{Binding HasPendingQuestion}"
Background="{DynamicResource AccentSoftBrush}"
BorderBrush="{DynamicResource AccentBrush}"
BorderThickness="0,0,0,1" Padding="12,8">
<StackPanel Spacing="6">
<TextBlock Classes="meta"
Text="{loc:Tr missionControl.question.title}"
Foreground="{DynamicResource AccentBrush}" FontWeight="SemiBold" />
<TextBlock Text="{Binding PendingQuestion}" TextWrapping="Wrap"
Foreground="{DynamicResource TextBrush}" />
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="6">
<TextBox Grid.Column="0"
Text="{Binding AnswerDraft, UpdateSourceTrigger=PropertyChanged}"
PlaceholderText="{loc:Tr missionControl.question.placeholder}"
AcceptsReturn="False">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding SubmitAnswerCommand}" />
</TextBox.KeyBindings>
</TextBox>
<Button Grid.Column="1"
Content="{loc:Tr missionControl.question.send}"
Command="{Binding SubmitAnswerCommand}" />
</Grid>
</StackPanel>
</Border>
<!-- Console body: reuse SessionTerminalView -->
<islands:SessionTerminalView
Entries="{Binding Log}"
Label="{Binding DisplayTitle}"
IsRunning="{Binding IsRunning}"
IsDone="{Binding IsDone}"
IsFailed="{Binding IsFailed}" />
</DockPanel>
</Border>
</UserControl>
@@ -1,26 +0,0 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.VisualTree;
using ClaudeDo.Ui.ViewModels.Islands;
namespace ClaudeDo.Ui.Views.MissionControl;
public partial class MonitorPaneView : UserControl
{
public MonitorPaneView() => InitializeComponent();
private async void OnHeaderPressed(object? sender, PointerPressedEventArgs e)
{
if (DataContext is not TaskMonitorViewModel m || m.SubscribedTaskId is not { } id) return;
if (e.Source is not Avalonia.Visual src) return;
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
// Don't start a drag when the press landed on a header action button.
var button = src as Button ?? src.FindAncestorOfType<Button>();
if (button is not null) return;
var data = new DataTransfer();
data.Add(DataTransferItem.Create(MissionControlView.PaneFormat, id));
await DragDrop.DoDragDropAsync(e, data, DragDropEffects.Move);
}
}
@@ -1,12 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:mc="using:ClaudeDo.Ui.Views.MissionControl"
x:Class="ClaudeDo.Ui.Views.MissionControl.TaskMonitorWindow"
x:DataType="vm:TaskMonitorViewModel"
Title="{Binding DisplayTitle}"
Width="560" Height="620" MinWidth="360" MinHeight="320"
Background="{DynamicResource VoidBrush}"
Icon="avares://ClaudeDo.Ui/Assets/ClaudeTask.ico">
<mc:MonitorPaneView />
</Window>
@@ -1,23 +0,0 @@
using System;
using Avalonia.Controls;
namespace ClaudeDo.Ui.Views.MissionControl;
public partial class TaskMonitorWindow : Window
{
public TaskMonitorWindow() => InitializeComponent();
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
if (DataContext is ClaudeDo.Ui.ViewModels.Islands.TaskMonitorViewModel vm)
vm.CloseWindowRequested = Close;
}
protected override void OnClosed(EventArgs e)
{
if (DataContext is ClaudeDo.Ui.ViewModels.Islands.TaskMonitorViewModel vm)
vm.CloseWindowRequested = null;
base.OnClosed(e);
}
}
@@ -9,7 +9,6 @@ using Avalonia.Media;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels;
using ClaudeDo.Ui.ViewModels.Conflicts;
using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.ViewModels.Modals;
using ClaudeDo.Ui.Views.Conflicts;
using ClaudeDo.Ui.Views.MissionControl;
@@ -146,13 +145,6 @@ public sealed class WindowDialogService : IDialogService
_missionControl.Activate(); // bring to front / focus
}
public void ShowDetachedMonitor(TaskMonitorViewModel monitor, Action onClosed)
{
var win = new TaskMonitorWindow { DataContext = monitor };
win.Closed += (_, _) => onClosed(); // closing re-docks into the grid
win.Show(); // modeless, independent
}
public Task<bool> ConfirmAsync(string message)
{
var tcs = new TaskCompletionSource<bool>();