fix(ui): show a banner when a ConPTY session dies after startup

InteractiveTerminalViewModel.HasExitedAfterStartup now covers every
post-startup death that ExitedAfterStartup alone missed (crash, clean
exit, quiet exit inside the grace window) -- previously the tile just
stayed looking alive/thinking with zero signal. ConPtyPaneView binds a
new banner to it (message + Retry + Close); CanRetry simplifies to
"any HasExited state" since the same descriptor factory can relaunch
either kind of death. HandlerRestartCoordinator's worker-side
auto-restart path is untouched.
This commit is contained in:
mika kuns
2026-09-10 13:32:19 +02:00
parent 2b0562b2ed
commit 256bea5ffa
7 changed files with 104 additions and 5 deletions
@@ -36,14 +36,29 @@ public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDispo
/// can show a spinner instead of an empty black pane.</summary>
public bool IsStarting => !IsRunning && !HasExited && StartError is null;
/// <summary>True once the process has died without a <see cref="StartError"/> already covering it —
/// i.e. every death that isn't a launch-time failure: a crash after running a while, a clean exit,
/// or a quiet exit inside the startup grace window. Drives the host's "session ended" banner so a
/// dead pane never looks like it's still running/thinking.</summary>
public bool HasExitedAfterStartup => HasExited && StartError is null;
partial void OnIsRunningChanged(bool value)
{
OnPropertyChanged(nameof(IsStarting));
if (value) _startedAtUtc = _utcNow();
}
partial void OnHasExitedChanged(bool value) => OnPropertyChanged(nameof(IsStarting));
partial void OnStartErrorChanged(string? value) => OnPropertyChanged(nameof(IsStarting));
partial void OnHasExitedChanged(bool value)
{
OnPropertyChanged(nameof(IsStarting));
OnPropertyChanged(nameof(HasExitedAfterStartup));
}
partial void OnStartErrorChanged(string? value)
{
OnPropertyChanged(nameof(IsStarting));
OnPropertyChanged(nameof(HasExitedAfterStartup));
}
public InteractiveTerminalViewModel() : this(() => DateTime.UtcNow)
{
@@ -2,6 +2,7 @@ using System;
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
namespace ClaudeDo.Ui.ViewModels.MissionControl;
@@ -137,8 +138,18 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IDisposable
SubmitForReviewCommand.NotifyCanExecuteChanged();
RetryCommand.NotifyCanExecuteChanged();
}
if (e.PropertyName is nameof(InteractiveTerminalViewModel.HasExitedAfterStartup)
or nameof(InteractiveTerminalViewModel.ExitCode))
{
OnPropertyChanged(nameof(ExitedBannerText));
}
}
/// <summary>"Session ended (code N)" text for <see cref="InteractiveTerminalViewModel.HasExitedAfterStartup"/> —
/// computed here (rather than formatted in XAML) since <c>loc:Tr</c> has no binding-arg support.</summary>
public string ExitedBannerText => Loc.T("missionControl.conptyExited", Terminal.ExitCode ?? 0);
/// <summary>Types a message into this pane's live session and submits it (the worker's
/// pre-usage-limit warning). False when the session isn't running.</summary>
public System.Threading.Tasks.Task<bool> SendLineAsync(string text) => Terminal.SendLineAsync(text);
@@ -167,9 +178,10 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IDisposable
partial void OnIsAbandonPendingChanged(bool value) => AbandonRunCommand.NotifyCanExecuteChanged();
// A launch failure permanently occupies the TaskId dedupe slot unless the user can retry —
// re-opening the same task would otherwise just re-focus a dead tile.
private bool CanRetry() => Terminal.HasExited && Terminal.StartError is not null;
// A dead session permanently occupies the TaskId dedupe slot unless the user can retry — whether
// it died at launch (StartError) or after (HasExitedAfterStartup), the same descriptor factory
// can always relaunch it, so any HasExited state is retryable.
private bool CanRetry() => Terminal.HasExited;
[RelayCommand(CanExecute = nameof(CanRetry))]
private void Retry()