feat(ui): open Mission Control from the title bar

This commit is contained in:
Mika Kuns
2026-06-25 15:05:40 +02:00
parent 283310a3fd
commit b1bd91292f
8 changed files with 44 additions and 3 deletions

View File

@@ -1,5 +1,6 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using ClaudeDo.Ui.Services;
@@ -32,6 +33,10 @@ public partial class App : Application
FocusClearing.Install();
// The main window is authoritative — closing it shuts the app down even if the
// modeless Mission Control window is still open.
desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
desktop.MainWindow = new MainWindow
{
DataContext = services.GetRequiredService<IslandsShellViewModel>(),

View File

@@ -168,8 +168,6 @@ sealed class Program
shell.ConflictResolverFactory =
sp.GetRequiredService<Func<string, ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel>>();
sp.GetRequiredService<MergeCoordinator>().Handler = shell.RequestConflictResolutionAsync;
var missionControl = sp.GetRequiredService<MissionControlViewModel>();
missionControl.OpenInApp = id => _ = shell.RevealTaskAsync(id);
return shell;
});

View File

@@ -21,6 +21,8 @@
<!-- Window control icons — filled geometries (PathIcon fills, not strokes) -->
<StreamGeometry x:Key="Icon.WinMin">M4 9 H16 V11 H4 Z</StreamGeometry>
<StreamGeometry x:Key="Icon.WinMax">M4 4 H16 V6 H4 Z M4 14 H16 V16 H4 Z M4 4 H6 V16 H4 Z M14 4 H16 V16 H14 Z</StreamGeometry>
<!-- Icon.Grid (four filled panes — Mission Control launcher) -->
<StreamGeometry x:Key="Icon.Grid">M3 3 H9 V9 H3 Z M11 3 H17 V9 H11 Z M3 11 H9 V17 H3 Z M11 11 H17 V17 H11 Z</StreamGeometry>
<StreamGeometry x:Key="Icon.WinRestore">M4 7 H13 V9 H4 Z M4 14 H13 V16 H4 Z M4 7 H6 V16 H4 Z M11 7 H13 V16 H11 Z M7 4 H16 V6 H7 Z M14 4 H16 V13 H14 Z</StreamGeometry>
<StreamGeometry x:Key="Icon.WinClose">M4 5 L5 4 L16 15 L15 16 Z M15 4 L16 5 L5 16 L4 15 Z</StreamGeometry>
<!-- Brand check glyph — filled rounded square with inset tick -->

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using ClaudeDo.Ui.ViewModels;
using ClaudeDo.Ui.ViewModels.Conflicts;
using ClaudeDo.Ui.ViewModels.Modals;
@@ -28,4 +29,7 @@ public interface IDialogService
/// <summary>Modal error notice with a single dismiss button.</summary>
Task ShowErrorAsync(string message);
/// <summary>Show (or re-show + focus) the modeless Mission Control window. Lazily created; hides on close.</summary>
void ShowMissionControl(MissionControlViewModel vm);
}

View File

@@ -20,6 +20,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
public TasksIslandViewModel? Tasks { get; }
public DetailsIslandViewModel? Details { get; }
public IWorkerClient? Worker { get; }
public MissionControlViewModel? MissionControl { get; }
public UpdateCheckService UpdateCheck => _updateCheck;
public string ConnectionText =>
@@ -206,9 +207,12 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
Func<WorktreesOverviewModalViewModel> worktreesOverviewVmFactory,
Func<WeeklyReportModalViewModel> weeklyReportVmFactory,
Func<MergeModalViewModel> mergeVmFactory,
Func<RepoImportModalViewModel> repoImportVmFactory)
Func<RepoImportModalViewModel> repoImportVmFactory,
MissionControlViewModel missionControl)
{
Lists = lists; Tasks = tasks; Details = details; Worker = worker;
MissionControl = missionControl;
MissionControl.OpenInApp = id => _ = RevealTaskAsync(id);
_updateCheck = updateCheck;
_installerLocator = installerLocator;
_workerLocator = workerLocator;
@@ -312,6 +316,13 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
if (InlineUpdateStatus == text) InlineUpdateStatus = null;
}
[RelayCommand]
private void OpenMissionControl()
{
if (Dialogs is not null && MissionControl is not null)
Dialogs.ShowMissionControl(MissionControl);
}
[RelayCommand]
private async Task OpenAbout()
{

View File

@@ -88,6 +88,11 @@
<!-- Right: window controls -->
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="0"
VerticalAlignment="Center" Margin="0,0,4,0">
<Button Classes="title-ctrl"
Command="{Binding OpenMissionControlCommand}"
ToolTip.Tip="{loc:Tr missionControl.windowTitle}">
<PathIcon Data="{StaticResource Icon.Grid}" Width="12" Height="12"/>
</Button>
<Button Classes="title-ctrl" Click="OnMinimize">
<PathIcon Data="{StaticResource Icon.WinMin}" Width="10" Height="10"/>
</Button>

View File

@@ -42,6 +42,12 @@ public partial class MainWindow : Window
if (DataContext is IslandsShellViewModel vm)
{
vm.Dialogs = new WindowDialogService(this);
vm.BringToFront = () =>
{
if (WindowState == WindowState.Minimized)
WindowState = WindowState.Normal;
Activate();
};
}
}

View File

@@ -8,6 +8,7 @@ using ClaudeDo.Ui.ViewModels;
using ClaudeDo.Ui.ViewModels.Conflicts;
using ClaudeDo.Ui.ViewModels.Modals;
using ClaudeDo.Ui.Views.Conflicts;
using ClaudeDo.Ui.Views.MissionControl;
using ClaudeDo.Ui.Views.Modals;
namespace ClaudeDo.Ui.Views;
@@ -22,6 +23,7 @@ namespace ClaudeDo.Ui.Views;
public sealed class WindowDialogService : IDialogService
{
private readonly Window _owner;
private MissionControlWindow? _missionControl;
public WindowDialogService(Window owner) => _owner = owner;
@@ -111,6 +113,14 @@ public sealed class WindowDialogService : IDialogService
await dlg.ShowDialog(_owner);
}
public void ShowMissionControl(MissionControlViewModel vm)
{
_missionControl ??= new MissionControlWindow { DataContext = vm };
if (!_missionControl.IsVisible)
_missionControl.Show(); // modeless, independent top-level window
_missionControl.Activate(); // bring to front / focus
}
public Task<bool> ConfirmAsync(string message)
{
var tcs = new TaskCompletionSource<bool>();