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.
103 lines
3.4 KiB
C#
103 lines
3.4 KiB
C#
using System;
|
|
using System.Threading.Tasks;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using ClaudeDo.Ui.Services;
|
|
using Iciclecreek.Terminal;
|
|
|
|
namespace ClaudeDo.Ui.ViewModels;
|
|
|
|
/// <summary>
|
|
/// Hosts a <see cref="PtyTerminalSession"/> for an embedded ConPTY terminal. The view attaches
|
|
/// its <see cref="TerminalControl"/> once loaded via <see cref="AttachControl"/>; <see cref="Start"/>
|
|
/// can be called before or after attach — whichever happens second triggers the launch.
|
|
/// </summary>
|
|
public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDisposable
|
|
{
|
|
private readonly PtyTerminalSession _session = new();
|
|
private TerminalControl? _control;
|
|
private TerminalLaunchDescriptor? _pendingDescriptor;
|
|
|
|
[ObservableProperty] private bool _isRunning;
|
|
[ObservableProperty] private bool _hasExited;
|
|
[ObservableProperty] private int? _exitCode;
|
|
[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()
|
|
{
|
|
_session.ProcessExited += OnSessionProcessExited;
|
|
}
|
|
|
|
/// <summary>Called by the view once its <see cref="TerminalControl"/> has loaded (template applied).</summary>
|
|
public void AttachControl(TerminalControl control)
|
|
{
|
|
_control = control;
|
|
if (_pendingDescriptor is { } descriptor)
|
|
{
|
|
_pendingDescriptor = null;
|
|
_ = StartCoreAsync(descriptor);
|
|
}
|
|
}
|
|
|
|
public void Start(TerminalLaunchDescriptor descriptor)
|
|
{
|
|
if (_control is null)
|
|
{
|
|
_pendingDescriptor = descriptor;
|
|
return;
|
|
}
|
|
_ = StartCoreAsync(descriptor);
|
|
}
|
|
|
|
private async Task StartCoreAsync(TerminalLaunchDescriptor descriptor)
|
|
{
|
|
if (_control is null) return;
|
|
try
|
|
{
|
|
StartError = null;
|
|
await _session.StartAsync(descriptor, _control);
|
|
IsRunning = true;
|
|
HasExited = false;
|
|
ExitCode = null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
IsRunning = false;
|
|
HasExited = true;
|
|
StartError = ex.Message;
|
|
}
|
|
}
|
|
|
|
private void OnSessionProcessExited(object? sender, int exitCode)
|
|
{
|
|
IsRunning = false;
|
|
HasExited = true;
|
|
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 Dispose()
|
|
{
|
|
_session.ProcessExited -= OnSessionProcessExited;
|
|
_session.Dispose();
|
|
}
|
|
}
|