Merge claudedo/ab96ad81d417402088aa669ed52259e9

This commit is contained in:
mika kuns
2026-08-06 14:41:08 +02:00
3 changed files with 98 additions and 3 deletions
@@ -13,9 +13,18 @@ namespace ClaudeDo.Ui.ViewModels;
/// </summary>
public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDisposable
{
// A process that dies within this window of becoming "running" is treated as a startup
// failure (see OnSessionProcessExited) rather than a normal session end, even though the
// spawn itself succeeded — e.g. `claude --session-id <guid>` exiting immediately on an
// auth/network hiccup. Long enough to clear normal CLI startup, short enough not to
// mistake a real crash-after-use for a start failure.
private static readonly TimeSpan StartupGraceWindow = TimeSpan.FromSeconds(5);
private readonly PtyTerminalSession _session = new();
private readonly Func<DateTime> _utcNow;
private TerminalControl? _control;
private TerminalLaunchDescriptor? _pendingDescriptor;
private DateTime? _startedAtUtc;
[ObservableProperty] private bool _isRunning;
[ObservableProperty] private bool _hasExited;
@@ -27,12 +36,23 @@ 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;
partial void OnIsRunningChanged(bool value) => OnPropertyChanged(nameof(IsStarting));
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));
public InteractiveTerminalViewModel()
public InteractiveTerminalViewModel() : this(() => DateTime.UtcNow)
{
}
/// <summary>Test-only seam for controlling "time since launch" without real delays.</summary>
internal InteractiveTerminalViewModel(Func<DateTime> utcNow)
{
_utcNow = utcNow;
_session.ProcessExited += OnSessionProcessExited;
}
@@ -76,11 +96,18 @@ public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDispo
}
}
private void OnSessionProcessExited(object? sender, int exitCode)
/// <summary>Internal (not private) so tests can drive it directly without a real ConPTY spawn.</summary>
internal void OnSessionProcessExited(object? sender, int exitCode)
{
IsRunning = false;
HasExited = true;
ExitCode = exitCode;
// Died at startup: route through the same banner as a launch-time failure instead of
// leaving a dead terminal with no Retry affordance (ConPtyPaneViewModel.CanRetry
// requires StartError).
if (exitCode != 0 && _startedAtUtc is { } startedAt && _utcNow() - startedAt < StartupGraceWindow)
StartError = $"Session exited immediately (code {exitCode}).";
}
/// <summary>Reports a failure that happened before <see cref="Start"/> could be called (e.g. the