diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index 3c01c959..195d61e7 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -160,7 +160,8 @@ "feedbackLabel": "FEEDBACK FÜR DEN AGENTEN", "feedbackPlaceholder": "Was soll der Agent korrigieren?", "rerun": "Erneut ausführen", - "refineTip": "Aufgabe mit Claude verfeinern" + "refineTip": "Aufgabe mit Claude verfeinern", + "refiningTip": "Claude verfeinert diese Aufgabe…" }, "lists": { "heading": "Listen", @@ -267,6 +268,7 @@ "overviewMode": "Übersicht", "closeSession": "Sitzung schließen", "conptyLaunchFailed": "ConPTY-Sitzung konnte nicht geöffnet werden: {0}", + "conptyStarting": "Sitzung wird gestartet…", "mergeHelperTitle": "Merge-Helfer", "submitForReviewFailed": "Einreichen zum Review fehlgeschlagen: {0}", "submitForReview": "Zum Review einreichen", diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index 5979f9e2..251147da 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -160,7 +160,8 @@ "feedbackLabel": "FEEDBACK FOR THE AGENT", "feedbackPlaceholder": "What should the agent fix?", "rerun": "Re-run", - "refineTip": "Refine this task with Claude" + "refineTip": "Refine this task with Claude", + "refiningTip": "Claude is refining this task…" }, "lists": { "heading": "Lists", @@ -267,6 +268,7 @@ "overviewMode": "Overview", "closeSession": "Close session", "conptyLaunchFailed": "Couldn't open ConPTY session: {0}", + "conptyStarting": "Starting session…", "mergeHelperTitle": "Merge Helper", "submitForReviewFailed": "Couldn't submit for review: {0}", "submitForReview": "Submit for review", diff --git a/src/ClaudeDo.Ui/Design/IslandStyles.axaml b/src/ClaudeDo.Ui/Design/IslandStyles.axaml index 8310bce7..3169b4e9 100644 --- a/src/ClaudeDo.Ui/Design/IslandStyles.axaml +++ b/src/ClaudeDo.Ui/Design/IslandStyles.axaml @@ -454,6 +454,25 @@ + + + + diff --git a/src/ClaudeDo.Ui/ViewModels/InteractiveTerminalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/InteractiveTerminalViewModel.cs index 6b335800..60813126 100644 --- a/src/ClaudeDo.Ui/ViewModels/InteractiveTerminalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/InteractiveTerminalViewModel.cs @@ -22,6 +22,15 @@ public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDispo [ObservableProperty] private int? _exitCode; [ObservableProperty] private string? _startError; + /// 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. + 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; @@ -74,6 +83,15 @@ public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDispo ExitCode = exitCode; } + /// Reports a failure that happened before could be called (e.g. the + /// launch-spec roundtrip threw), so it surfaces through the same banner as a spawn failure. + public void ReportStartFailure(string message) + { + IsRunning = false; + HasExited = true; + StartError = message; + } + public void Kill() => _session.Kill(); public void Dispose() diff --git a/src/ClaudeDo.Ui/ViewModels/MissionControl/ConPtyPaneViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MissionControl/ConPtyPaneViewModel.cs index 40125658..23257b72 100644 --- a/src/ClaudeDo.Ui/ViewModels/MissionControl/ConPtyPaneViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/MissionControl/ConPtyPaneViewModel.cs @@ -35,18 +35,44 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControl public event Action? SubmitForReviewRequested; /// Task-based pane — dedup'd by . Pass null for an ad-hoc pane - /// (no task, never deduped); prefer at ad-hoc call sites. - public ConPtyPaneViewModel(string? taskId, string displayTitle, TerminalLaunchDescriptor descriptor) + /// (no task, never deduped); prefer at ad-hoc call sites. + /// is resolved by the pane itself (in ) + /// 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 . + public ConPtyPaneViewModel( + string? taskId, + string displayTitle, + Func> descriptorFactory) { TaskId = taskId; _displayTitle = displayTitle; + _descriptorFactory = descriptorFactory; Terminal.PropertyChanged += OnTerminalPropertyChanged; - Terminal.Start(descriptor); + } + + private readonly Func> _descriptorFactory; + + /// Resolves the launch spec and spawns the session. Call after wiring + /// so a failed launch is not swallowed. + public void Start() => _ = StartAsync(); + + private async System.Threading.Tasks.Task StartAsync() + { + try + { + Terminal.Start(await _descriptorFactory()); + } + catch (Exception ex) + { + Terminal.ReportStartFailure(ex.Message); + } } /// Ad-hoc pane — no task, no dedup. - public static ConPtyPaneViewModel CreateAdHoc(string displayTitle, TerminalLaunchDescriptor descriptor) - => new(null, displayTitle, descriptor); + public static ConPtyPaneViewModel CreateAdHoc( + string displayTitle, + Func> descriptorFactory) + => new(null, displayTitle, descriptorFactory); private void OnTerminalPropertyChanged(object? sender, PropertyChangedEventArgs e) { diff --git a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs index 5792365b..650bf90b 100644 --- a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs @@ -241,20 +241,8 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable } catch { /* best-effort title lookup */ } - try - { - 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)); - } + AddConPtyPane(new ConPtyPaneViewModel(taskId, title, + () => DescribeAsync(() => _worker.GetInteractiveLaunchSpecAsync(taskId)))); } // 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 */ } title += Loc.T("missionControl.planningTitleSuffix"); - try - { - var spec = resume - ? 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)); - } + AddConPtyPane(new ConPtyPaneViewModel(taskId, title, () => DescribeAsync(() => resume + ? _worker.GetPlanningResumeLaunchSpecAsync(taskId) + : _worker.GetPlanningStartLaunchSpecAsync(taskId)))); } // 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. - 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('\\', '/')); if (string.IsNullOrEmpty(title)) title = directory; - try - { - var spec = await _worker.GetAdHocLaunchSpecAsync(directory); - 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)); - } + AddConPtyPane(ConPtyPaneViewModel.CreateAdHoc(title, + () => DescribeAsync(() => _worker.GetAdHocLaunchSpecAsync(directory)))); + return System.Threading.Tasks.Task.CompletedTask; } // 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 */ } - try - { - 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)); - } + AddConPtyPane(ConPtyPaneViewModel.CreateAdHoc(title, + () => DescribeAsync(() => _worker.GetMergeHelperLaunchSpecAsync(taskIds, listId)))); } - 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 DescribeAsync( + Func> 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 // session is finished). The worker commits the worktree and moves the task to WaitingForReview. diff --git a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml index db880640..4329eef1 100644 --- a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml @@ -207,17 +207,23 @@ - - + + + + +