feat(ui): spinners for ConPTY session start and task refine
Both actions previously gave no feedback: opening a ConPTY session only created the tile after the launch-spec roundtrip (which may build a worktree), and the refine button just disappeared while the run was in flight. Add a shared Ellipse.spinner style, and let ConPtyPaneViewModel resolve its own launch spec so the tile shows up immediately with a starting overlay. A failed launch now keeps the tile with its inline error banner instead of never appearing — Start() is separated from the ctor so the host can subscribe to ErrorReported before the launch begins.
This commit is contained in:
@@ -160,7 +160,8 @@
|
|||||||
"feedbackLabel": "FEEDBACK FÜR DEN AGENTEN",
|
"feedbackLabel": "FEEDBACK FÜR DEN AGENTEN",
|
||||||
"feedbackPlaceholder": "Was soll der Agent korrigieren?",
|
"feedbackPlaceholder": "Was soll der Agent korrigieren?",
|
||||||
"rerun": "Erneut ausführen",
|
"rerun": "Erneut ausführen",
|
||||||
"refineTip": "Aufgabe mit Claude verfeinern"
|
"refineTip": "Aufgabe mit Claude verfeinern",
|
||||||
|
"refiningTip": "Claude verfeinert diese Aufgabe…"
|
||||||
},
|
},
|
||||||
"lists": {
|
"lists": {
|
||||||
"heading": "Listen",
|
"heading": "Listen",
|
||||||
@@ -267,6 +268,7 @@
|
|||||||
"overviewMode": "Übersicht",
|
"overviewMode": "Übersicht",
|
||||||
"closeSession": "Sitzung schließen",
|
"closeSession": "Sitzung schließen",
|
||||||
"conptyLaunchFailed": "ConPTY-Sitzung konnte nicht geöffnet werden: {0}",
|
"conptyLaunchFailed": "ConPTY-Sitzung konnte nicht geöffnet werden: {0}",
|
||||||
|
"conptyStarting": "Sitzung wird gestartet…",
|
||||||
"mergeHelperTitle": "Merge-Helfer",
|
"mergeHelperTitle": "Merge-Helfer",
|
||||||
"submitForReviewFailed": "Einreichen zum Review fehlgeschlagen: {0}",
|
"submitForReviewFailed": "Einreichen zum Review fehlgeschlagen: {0}",
|
||||||
"submitForReview": "Zum Review einreichen",
|
"submitForReview": "Zum Review einreichen",
|
||||||
|
|||||||
@@ -160,7 +160,8 @@
|
|||||||
"feedbackLabel": "FEEDBACK FOR THE AGENT",
|
"feedbackLabel": "FEEDBACK FOR THE AGENT",
|
||||||
"feedbackPlaceholder": "What should the agent fix?",
|
"feedbackPlaceholder": "What should the agent fix?",
|
||||||
"rerun": "Re-run",
|
"rerun": "Re-run",
|
||||||
"refineTip": "Refine this task with Claude"
|
"refineTip": "Refine this task with Claude",
|
||||||
|
"refiningTip": "Claude is refining this task…"
|
||||||
},
|
},
|
||||||
"lists": {
|
"lists": {
|
||||||
"heading": "Lists",
|
"heading": "Lists",
|
||||||
@@ -267,6 +268,7 @@
|
|||||||
"overviewMode": "Overview",
|
"overviewMode": "Overview",
|
||||||
"closeSession": "Close session",
|
"closeSession": "Close session",
|
||||||
"conptyLaunchFailed": "Couldn't open ConPTY session: {0}",
|
"conptyLaunchFailed": "Couldn't open ConPTY session: {0}",
|
||||||
|
"conptyStarting": "Starting session…",
|
||||||
"mergeHelperTitle": "Merge Helper",
|
"mergeHelperTitle": "Merge Helper",
|
||||||
"submitForReviewFailed": "Couldn't submit for review: {0}",
|
"submitForReviewFailed": "Couldn't submit for review: {0}",
|
||||||
"submitForReview": "Submit for review",
|
"submitForReview": "Submit for review",
|
||||||
|
|||||||
@@ -454,6 +454,25 @@
|
|||||||
</Style.Animations>
|
</Style.Animations>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- Indeterminate ring spinner (dashed ring, rotated forever) -->
|
||||||
|
<Style Selector="Ellipse.spinner">
|
||||||
|
<Setter Property="Width" Value="14" />
|
||||||
|
<Setter Property="Height" Value="14" />
|
||||||
|
<Setter Property="Stroke" Value="{StaticResource AccentBrush}" />
|
||||||
|
<Setter Property="StrokeThickness" Value="2" />
|
||||||
|
<Setter Property="StrokeDashArray" Value="3,2" />
|
||||||
|
<Setter Property="RenderTransform" Value="rotate(0deg)" />
|
||||||
|
<Style.Animations>
|
||||||
|
<Animation Duration="0:0:0.9" IterationCount="INFINITE" Easing="LinearEasing">
|
||||||
|
<KeyFrame Cue="0%"> <Setter Property="RotateTransform.Angle" Value="0" /> </KeyFrame>
|
||||||
|
<KeyFrame Cue="100%"><Setter Property="RotateTransform.Angle" Value="360" /></KeyFrame>
|
||||||
|
</Animation>
|
||||||
|
</Style.Animations>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Ellipse.spinner.dim">
|
||||||
|
<Setter Property="Stroke" Value="{StaticResource TextDimBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
<!-- AGENT STRIP -->
|
<!-- AGENT STRIP -->
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDispo
|
|||||||
[ObservableProperty] private int? _exitCode;
|
[ObservableProperty] private int? _exitCode;
|
||||||
[ObservableProperty] private string? _startError;
|
[ObservableProperty] private string? _startError;
|
||||||
|
|
||||||
|
/// <summary>True from construction until the child process is actually launched (or the launch
|
||||||
|
/// failed) — covers both the caller's launch-spec roundtrip and the ConPTY spawn, so the host
|
||||||
|
/// can show a spinner instead of an empty black pane.</summary>
|
||||||
|
public bool IsStarting => !IsRunning && !HasExited && StartError is null;
|
||||||
|
|
||||||
|
partial void OnIsRunningChanged(bool value) => OnPropertyChanged(nameof(IsStarting));
|
||||||
|
partial void OnHasExitedChanged(bool value) => OnPropertyChanged(nameof(IsStarting));
|
||||||
|
partial void OnStartErrorChanged(string? value) => OnPropertyChanged(nameof(IsStarting));
|
||||||
|
|
||||||
public InteractiveTerminalViewModel()
|
public InteractiveTerminalViewModel()
|
||||||
{
|
{
|
||||||
_session.ProcessExited += OnSessionProcessExited;
|
_session.ProcessExited += OnSessionProcessExited;
|
||||||
@@ -74,6 +83,15 @@ public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDispo
|
|||||||
ExitCode = exitCode;
|
ExitCode = exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Reports a failure that happened before <see cref="Start"/> could be called (e.g. the
|
||||||
|
/// launch-spec roundtrip threw), so it surfaces through the same banner as a spawn failure.</summary>
|
||||||
|
public void ReportStartFailure(string message)
|
||||||
|
{
|
||||||
|
IsRunning = false;
|
||||||
|
HasExited = true;
|
||||||
|
StartError = message;
|
||||||
|
}
|
||||||
|
|
||||||
public void Kill() => _session.Kill();
|
public void Kill() => _session.Kill();
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -35,18 +35,44 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControl
|
|||||||
public event Action<string>? SubmitForReviewRequested;
|
public event Action<string>? SubmitForReviewRequested;
|
||||||
|
|
||||||
/// <summary>Task-based pane — dedup'd by <see cref="TaskId"/>. Pass null for an ad-hoc pane
|
/// <summary>Task-based pane — dedup'd by <see cref="TaskId"/>. Pass null for an ad-hoc pane
|
||||||
/// (no task, never deduped); prefer <see cref="CreateAdHoc"/> at ad-hoc call sites.</summary>
|
/// (no task, never deduped); prefer <see cref="CreateAdHoc"/> at ad-hoc call sites.
|
||||||
public ConPtyPaneViewModel(string? taskId, string displayTitle, TerminalLaunchDescriptor descriptor)
|
/// <paramref name="descriptorFactory"/> is resolved by the pane itself (in <see cref="Start"/>)
|
||||||
|
/// so the tile — and its starting spinner — is visible while the launch spec is still being
|
||||||
|
/// fetched. The host must wire its handlers and then call <see cref="Start"/>.</summary>
|
||||||
|
public ConPtyPaneViewModel(
|
||||||
|
string? taskId,
|
||||||
|
string displayTitle,
|
||||||
|
Func<System.Threading.Tasks.Task<TerminalLaunchDescriptor>> descriptorFactory)
|
||||||
{
|
{
|
||||||
TaskId = taskId;
|
TaskId = taskId;
|
||||||
_displayTitle = displayTitle;
|
_displayTitle = displayTitle;
|
||||||
|
_descriptorFactory = descriptorFactory;
|
||||||
Terminal.PropertyChanged += OnTerminalPropertyChanged;
|
Terminal.PropertyChanged += OnTerminalPropertyChanged;
|
||||||
Terminal.Start(descriptor);
|
}
|
||||||
|
|
||||||
|
private readonly Func<System.Threading.Tasks.Task<TerminalLaunchDescriptor>> _descriptorFactory;
|
||||||
|
|
||||||
|
/// <summary>Resolves the launch spec and spawns the session. Call after wiring
|
||||||
|
/// <see cref="ErrorReported"/> so a failed launch is not swallowed.</summary>
|
||||||
|
public void Start() => _ = StartAsync();
|
||||||
|
|
||||||
|
private async System.Threading.Tasks.Task StartAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Terminal.Start(await _descriptorFactory());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Terminal.ReportStartFailure(ex.Message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Ad-hoc pane — no task, no dedup.</summary>
|
/// <summary>Ad-hoc pane — no task, no dedup.</summary>
|
||||||
public static ConPtyPaneViewModel CreateAdHoc(string displayTitle, TerminalLaunchDescriptor descriptor)
|
public static ConPtyPaneViewModel CreateAdHoc(
|
||||||
=> new(null, displayTitle, descriptor);
|
string displayTitle,
|
||||||
|
Func<System.Threading.Tasks.Task<TerminalLaunchDescriptor>> descriptorFactory)
|
||||||
|
=> new(null, displayTitle, descriptorFactory);
|
||||||
|
|
||||||
private void OnTerminalPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
private void OnTerminalPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -241,20 +241,8 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
}
|
}
|
||||||
catch { /* best-effort title lookup */ }
|
catch { /* best-effort title lookup */ }
|
||||||
|
|
||||||
try
|
AddConPtyPane(new ConPtyPaneViewModel(taskId, title,
|
||||||
{
|
() => DescribeAsync(() => _worker.GetInteractiveLaunchSpecAsync(taskId))));
|
||||||
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;
|
|
||||||
pane.SubmitForReviewRequested += OnPaneSubmitForReview;
|
|
||||||
ConPtySessions.Add(pane);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
ErrorReported?.Invoke(Loc.T("missionControl.conptyLaunchFailed", ex.Message));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts (or resumes) a planning session and hosts it as an embedded ConPTY Command Center
|
// Starts (or resumes) a planning session and hosts it as an embedded ConPTY Command Center
|
||||||
@@ -279,47 +267,23 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
catch { /* best-effort title lookup */ }
|
catch { /* best-effort title lookup */ }
|
||||||
title += Loc.T("missionControl.planningTitleSuffix");
|
title += Loc.T("missionControl.planningTitleSuffix");
|
||||||
|
|
||||||
try
|
AddConPtyPane(new ConPtyPaneViewModel(taskId, title, () => DescribeAsync(() => resume
|
||||||
{
|
? _worker.GetPlanningResumeLaunchSpecAsync(taskId)
|
||||||
var spec = resume
|
: _worker.GetPlanningStartLaunchSpecAsync(taskId))));
|
||||||
? await _worker.GetPlanningResumeLaunchSpecAsync(taskId)
|
|
||||||
: await _worker.GetPlanningStartLaunchSpecAsync(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;
|
|
||||||
pane.SubmitForReviewRequested += OnPaneSubmitForReview;
|
|
||||||
ConPtySessions.Add(pane);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
ErrorReported?.Invoke(Loc.T("missionControl.conptyLaunchFailed", ex.Message));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ad-hoc (task-less) ConPTY session in a user-chosen directory. Never deduped — every call
|
// Ad-hoc (task-less) ConPTY session in a user-chosen directory. Never deduped — every call
|
||||||
// opens a fresh pane, unlike the task-based OpenConPtySessionAsync above.
|
// opens a fresh pane, unlike the task-based OpenConPtySessionAsync above.
|
||||||
public async System.Threading.Tasks.Task OpenAdHocConPtySessionAsync(string directory)
|
public System.Threading.Tasks.Task OpenAdHocConPtySessionAsync(string directory)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(directory)) return;
|
if (string.IsNullOrEmpty(directory)) return System.Threading.Tasks.Task.CompletedTask;
|
||||||
|
|
||||||
var title = Path.GetFileName(directory.TrimEnd('\\', '/'));
|
var title = Path.GetFileName(directory.TrimEnd('\\', '/'));
|
||||||
if (string.IsNullOrEmpty(title)) title = directory;
|
if (string.IsNullOrEmpty(title)) title = directory;
|
||||||
|
|
||||||
try
|
AddConPtyPane(ConPtyPaneViewModel.CreateAdHoc(title,
|
||||||
{
|
() => DescribeAsync(() => _worker.GetAdHocLaunchSpecAsync(directory))));
|
||||||
var spec = await _worker.GetAdHocLaunchSpecAsync(directory);
|
return System.Threading.Tasks.Task.CompletedTask;
|
||||||
var descriptor = new TerminalLaunchDescriptor(spec.Cwd, spec.Exe, spec.Args, spec.Env);
|
|
||||||
var pane = ConPtyPaneViewModel.CreateAdHoc(title, descriptor);
|
|
||||||
pane.ErrorReported += OnConPtyPaneError;
|
|
||||||
pane.CloseRequested += CloseConPtySession;
|
|
||||||
pane.SubmitForReviewRequested += OnPaneSubmitForReview;
|
|
||||||
ConPtySessions.Add(pane);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
ErrorReported?.Invoke(Loc.T("missionControl.conptyLaunchFailed", ex.Message));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// List-handler session over a hand-picked set of tasks ("Let Claude handle it").
|
// List-handler session over a hand-picked set of tasks ("Let Claude handle it").
|
||||||
@@ -337,23 +301,30 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
}
|
}
|
||||||
catch { /* best-effort title lookup */ }
|
catch { /* best-effort title lookup */ }
|
||||||
|
|
||||||
try
|
AddConPtyPane(ConPtyPaneViewModel.CreateAdHoc(title,
|
||||||
{
|
() => DescribeAsync(() => _worker.GetMergeHelperLaunchSpecAsync(taskIds, listId))));
|
||||||
var spec = await _worker.GetMergeHelperLaunchSpecAsync(taskIds, listId);
|
|
||||||
var descriptor = new TerminalLaunchDescriptor(spec.Cwd, spec.Exe, spec.Args, spec.Env);
|
|
||||||
var pane = ConPtyPaneViewModel.CreateAdHoc(title, descriptor);
|
|
||||||
pane.ErrorReported += OnConPtyPaneError;
|
|
||||||
pane.CloseRequested += CloseConPtySession;
|
|
||||||
pane.SubmitForReviewRequested += OnPaneSubmitForReview;
|
|
||||||
ConPtySessions.Add(pane);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
ErrorReported?.Invoke(Loc.T("missionControl.conptyLaunchFailed", ex.Message));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnConPtyPaneError(string message) => ErrorReported?.Invoke(message);
|
// Wires a freshly built pane and shows it immediately — the pane resolves its own launch spec,
|
||||||
|
// so the tile is on screen (spinner running) while the worker is still preparing the worktree.
|
||||||
|
private void AddConPtyPane(ConPtyPaneViewModel pane)
|
||||||
|
{
|
||||||
|
pane.ErrorReported += OnConPtyPaneError;
|
||||||
|
pane.CloseRequested += CloseConPtySession;
|
||||||
|
pane.SubmitForReviewRequested += OnPaneSubmitForReview;
|
||||||
|
ConPtySessions.Add(pane);
|
||||||
|
pane.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async System.Threading.Tasks.Task<TerminalLaunchDescriptor> DescribeAsync(
|
||||||
|
Func<System.Threading.Tasks.Task<LaunchSpec>> fetch)
|
||||||
|
{
|
||||||
|
var spec = await fetch();
|
||||||
|
return new TerminalLaunchDescriptor(spec.Cwd, spec.Exe, spec.Args, spec.Env);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnConPtyPaneError(string message)
|
||||||
|
=> ErrorReported?.Invoke(Loc.T("missionControl.conptyLaunchFailed", message));
|
||||||
|
|
||||||
// Submit a task's hand-driven ConPTY work for review, then close the pane (the interactive
|
// Submit a task's hand-driven ConPTY work for review, then close the pane (the interactive
|
||||||
// session is finished). The worker commits the worktree and moves the task to WaitingForReview.
|
// session is finished). The worker commits the worktree and moves the task to WaitingForReview.
|
||||||
|
|||||||
@@ -207,17 +207,23 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Refine button -->
|
<!-- Refine button, replaced by a spinner while the refine run is in flight -->
|
||||||
<Button Grid.Column="5" Classes="icon-btn refine-btn"
|
<Panel Grid.Column="5">
|
||||||
IsVisible="{Binding CanRefine}"
|
<Button Classes="icon-btn refine-btn"
|
||||||
VerticalAlignment="Top" Margin="0,2,0,0"
|
IsVisible="{Binding CanRefine}"
|
||||||
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).RefineTaskCommand}"
|
VerticalAlignment="Top" Margin="0,2,0,0"
|
||||||
CommandParameter="{Binding}"
|
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).RefineTaskCommand}"
|
||||||
ToolTip.Tip="{loc:Tr tasks.refineTip}">
|
CommandParameter="{Binding}"
|
||||||
<Viewbox Width="16" Height="16">
|
ToolTip.Tip="{loc:Tr tasks.refineTip}">
|
||||||
<Path Classes="plan-icon" Data="{StaticResource Icon.Refine}"/>
|
<Viewbox Width="16" Height="16">
|
||||||
</Viewbox>
|
<Path Classes="plan-icon" Data="{StaticResource Icon.Refine}"/>
|
||||||
</Button>
|
</Viewbox>
|
||||||
|
</Button>
|
||||||
|
<Ellipse Classes="spinner"
|
||||||
|
IsVisible="{Binding IsRefining}"
|
||||||
|
VerticalAlignment="Top" Margin="0,4,2,0"
|
||||||
|
ToolTip.Tip="{loc:Tr tasks.refiningTip}"/>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
<!-- Star toggle -->
|
<!-- Star toggle -->
|
||||||
<Button Grid.Column="6" Classes="icon-btn star-btn"
|
<Button Grid.Column="6" Classes="icon-btn star-btn"
|
||||||
|
|||||||
@@ -44,8 +44,20 @@
|
|||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Embedded ConPTY terminal -->
|
<!-- Embedded ConPTY terminal, with a starting overlay until the session is spawned -->
|
||||||
<views:InteractiveTerminalView DataContext="{Binding Terminal}" />
|
<Panel>
|
||||||
|
<views:InteractiveTerminalView DataContext="{Binding Terminal}" />
|
||||||
|
<Border IsVisible="{Binding Terminal.IsStarting}"
|
||||||
|
Background="{DynamicResource VoidBrush}">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="10"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<Ellipse Classes="spinner"/>
|
||||||
|
<TextBlock Classes="meta" Text="{loc:Tr missionControl.conptyStarting}"
|
||||||
|
Foreground="{DynamicResource TextDimBrush}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ public class MissionControlViewModelTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenConPtySessionAsync_WorkerThrows_RaisesErrorReported_NoPaneAdded()
|
public async Task OpenConPtySessionAsync_WorkerThrows_RaisesErrorReported_PaneShowsFailure()
|
||||||
{
|
{
|
||||||
var worker = new ThrowingLaunchSpecWorker();
|
var worker = new ThrowingLaunchSpecWorker();
|
||||||
using var vm = BuildVm(worker);
|
using var vm = BuildVm(worker);
|
||||||
@@ -307,8 +307,10 @@ public class MissionControlViewModelTests : IDisposable
|
|||||||
|
|
||||||
await vm.OpenConPtySessionAsync("t1");
|
await vm.OpenConPtySessionAsync("t1");
|
||||||
|
|
||||||
Assert.Empty(vm.ConPtySessions);
|
// The tile is shown up-front (spinner) and keeps the failure inline rather than vanishing.
|
||||||
Assert.Empty(vm.Panes);
|
var pane = Assert.Single(vm.ConPtySessions);
|
||||||
|
Assert.NotNull(pane.Terminal.StartError);
|
||||||
|
Assert.False(pane.Terminal.IsStarting);
|
||||||
Assert.NotNull(error);
|
Assert.NotNull(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +380,7 @@ public class MissionControlViewModelTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenAdHocConPtySessionAsync_WorkerThrows_RaisesErrorReported_NoPaneAdded()
|
public async Task OpenAdHocConPtySessionAsync_WorkerThrows_RaisesErrorReported_PaneShowsFailure()
|
||||||
{
|
{
|
||||||
var worker = new ThrowingAdHocLaunchSpecWorker();
|
var worker = new ThrowingAdHocLaunchSpecWorker();
|
||||||
using var vm = BuildVm(worker);
|
using var vm = BuildVm(worker);
|
||||||
@@ -387,8 +389,9 @@ public class MissionControlViewModelTests : IDisposable
|
|||||||
|
|
||||||
await vm.OpenAdHocConPtySessionAsync(Path.Combine("C:", "Some", "Path"));
|
await vm.OpenAdHocConPtySessionAsync(Path.Combine("C:", "Some", "Path"));
|
||||||
|
|
||||||
Assert.Empty(vm.ConPtySessions);
|
var pane = Assert.Single(vm.ConPtySessions);
|
||||||
Assert.Empty(vm.Panes);
|
Assert.NotNull(pane.Terminal.StartError);
|
||||||
|
Assert.False(pane.Terminal.IsStarting);
|
||||||
Assert.NotNull(error);
|
Assert.NotNull(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,7 +454,7 @@ public class MissionControlViewModelTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenMergeHelperConPtySessionAsync_WorkerThrows_RaisesErrorReported_NoPaneAdded()
|
public async Task OpenMergeHelperConPtySessionAsync_WorkerThrows_RaisesErrorReported_PaneShowsFailure()
|
||||||
{
|
{
|
||||||
var worker = new ThrowingMergeHelperLaunchSpecWorker();
|
var worker = new ThrowingMergeHelperLaunchSpecWorker();
|
||||||
using var vm = BuildVm(worker);
|
using var vm = BuildVm(worker);
|
||||||
@@ -460,8 +463,9 @@ public class MissionControlViewModelTests : IDisposable
|
|||||||
|
|
||||||
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1" });
|
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1" });
|
||||||
|
|
||||||
Assert.Empty(vm.ConPtySessions);
|
var pane = Assert.Single(vm.ConPtySessions);
|
||||||
Assert.Empty(vm.Panes);
|
Assert.NotNull(pane.Terminal.StartError);
|
||||||
|
Assert.False(pane.Terminal.IsStarting);
|
||||||
Assert.NotNull(error);
|
Assert.NotNull(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user