From 109a35c50570a4716aa1823a2674cfa3a0461ae8 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Fri, 24 Jul 2026 13:05:22 +0200 Subject: [PATCH] feat(review): submit interactive (ConPTY) work for review An embedded ConPTY session leaves its worktree changed but never touches task status, so hand-driven work had no path into the review/merge flow. Add SubmitTaskForReview: commit the worktree (same auto-commit as a headless run), then transition Idle/Failed -> WaitingForReview via the new TaskStateService.SubmitInteractiveForReviewAsync. Approve then merges it. Surfaces: a 'Submit for review' button in the detail work console (shown for an Idle/Failed task with a worktree) and on the ConPTY Command Center pane header (task-based panes; closes the pane on success). Tests cover the new transition (Idle/Failed accepted, Running/Queued/Done/Review rejected). --- src/ClaudeDo.Localization/locales/de.json | 3 ++ src/ClaudeDo.Localization/locales/en.json | 3 ++ .../Services/Interfaces/IWorkerClient.cs | 3 ++ src/ClaudeDo.Ui/Services/WorkerClient.cs | 3 ++ .../Islands/DetailsIslandViewModel.cs | 21 ++++++++++ .../MissionControl/ConPtyPaneViewModel.cs | 15 +++++++ .../ViewModels/MissionControlViewModel.cs | 19 +++++++++ .../Views/Islands/Detail/WorkConsole.axaml | 11 ++++++ .../Views/MissionControl/ConPtyPaneView.axaml | 9 ++++- src/ClaudeDo.Worker/Hub/WorkerHub.cs | 39 ++++++++++++++++++- .../State/Interfaces/ITaskStateService.cs | 1 + src/ClaudeDo.Worker/State/TaskStateService.cs | 20 ++++++++++ tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs | 1 + .../State/ReviewTransitionTests.cs | 32 +++++++++++++++ .../UiVm/TasksIslandViewModelPlanningTests.cs | 1 + 15 files changed, 178 insertions(+), 3 deletions(-) diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index 8753be16..5c806957 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -266,6 +266,9 @@ "overviewMode": "Übersicht", "closeSession": "Sitzung schließen", "conptyLaunchFailed": "ConPTY-Sitzung konnte nicht geöffnet werden: {0}", + "submitForReviewFailed": "Einreichen zum Review fehlgeschlagen: {0}", + "submitForReview": "Zum Review einreichen", + "submitForReviewTip": "Diesen Worktree committen und den Task ins Review bringen, damit er gemergt werden kann", "planningTitleSuffix": " (Planung)", "question": { "title": "Claude fragt nach", diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index 9a82ee24..5e1c1b7e 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -266,6 +266,9 @@ "overviewMode": "Overview", "closeSession": "Close session", "conptyLaunchFailed": "Couldn't open ConPTY session: {0}", + "submitForReviewFailed": "Couldn't submit for review: {0}", + "submitForReview": "Submit for review", + "submitForReviewTip": "Commit this worktree and move the task to review so it can be merged", "planningTitleSuffix": " (Planning)", "question": { "title": "Claude is asking", diff --git a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs index b0e32e93..3e0f8ebf 100644 --- a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs @@ -76,6 +76,9 @@ public interface IWorkerClient : INotifyPropertyChanged Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default); // Picks up a task's Claude session in a real terminal window (--resume). Task ResumeTaskInTerminalAsync(string taskId, CancellationToken ct = default); + /// Commits an interactively-worked task's worktree and moves it to WaitingForReview + /// (the only path that flips a hand-driven ConPTY session into the review/merge pipeline). + Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default); /// Launch spec for an embedded ConPTY terminal to open an interactive session /// in a task's worktree (same worktree prep as an autonomous run). Task GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default); diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs index 56768cc4..5ce7adf6 100644 --- a/src/ClaudeDo.Ui/Services/WorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs @@ -513,6 +513,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC public async Task ResumeTaskInTerminalAsync(string taskId, CancellationToken ct = default) => await _hub.InvokeAsync("ResumeTaskInTerminal", taskId, ct); + public async Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default) + => await _hub.InvokeAsync("SubmitTaskForReview", taskId, ct); + public async Task GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default) => await _hub.InvokeAsync("GetInteractiveLaunchSpec", taskId, ct); diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index 7c6b7647..604ed891 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -413,6 +413,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable ReviewDiffViewed = false; ApproveReviewCommand.NotifyCanExecuteChanged(); OnPropertyChanged(nameof(ShowReviewDiffHint)); + OnPropertyChanged(nameof(CanSubmitForReview)); + SubmitForReviewCommand.NotifyCanExecuteChanged(); AgentSettings.IsRunning = IsRunning; NotifySessionSections(); OnPropertyChanged(nameof(CanAcceptDrop)); @@ -758,6 +760,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable Merge.SyncWorktree(WorktreePath, WorktreeBaseCommit, WorktreeHeadCommit, WorktreeStateLabel, _listWorkingDir); NotifySessionSections(); + OnPropertyChanged(nameof(CanSubmitForReview)); + SubmitForReviewCommand.NotifyCanExecuteChanged(); } partial void OnWorktreeHeadCommitChanged(string? value) => @@ -1034,6 +1038,23 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable Task != null && _worker.IsConnected && IsWaitingForReview && (!Merge.HasReviewableDiff || ReviewDiffViewed); + // An interactive (ConPTY) session leaves its worktree changed but never flips the task + // status. Offer "Submit for review" for an Idle/Failed task that still has a worktree, so + // its hand-driven work can enter the normal review/merge flow (commits first, server-side). + public bool CanSubmitForReview => + Task != null && _worker.IsConnected && (IsIdle || IsFailed) && !string.IsNullOrEmpty(WorktreePath); + + [RelayCommand(CanExecute = nameof(CanSubmitForReview))] + private async System.Threading.Tasks.Task SubmitForReviewAsync() + { + if (Task is null || !_worker.IsConnected) return; + try { await _worker.SubmitTaskForReviewAsync(Task.Id); } + catch (Exception ex) + { + if (ShowErrorAsync != null) await ShowErrorAsync(ex.Message); + } + } + [RelayCommand(CanExecute = nameof(HasReviewFeedback))] private async System.Threading.Tasks.Task RejectReviewAsync() { diff --git a/src/ClaudeDo.Ui/ViewModels/MissionControl/ConPtyPaneViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MissionControl/ConPtyPaneViewModel.cs index 3ae6ebef..40125658 100644 --- a/src/ClaudeDo.Ui/ViewModels/MissionControl/ConPtyPaneViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/MissionControl/ConPtyPaneViewModel.cs @@ -17,6 +17,9 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControl { public string? TaskId { get; } + // Only a task-based pane can be submitted for review (an ad-hoc directory session has no task). + public bool IsTaskBased => TaskId is not null; + [ObservableProperty] private string _displayTitle; public InteractiveTerminalViewModel Terminal { get; } = new(); @@ -27,6 +30,10 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControl /// Set by the host (Mission Control) to remove this pane from its collection. public Action? CloseRequested { get; set; } + /// Raised when the user submits this task's hand-driven work for review; the host + /// commits the worktree and moves the task to WaitingForReview. + 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) @@ -50,6 +57,14 @@ public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControl [RelayCommand] private void Close() => CloseRequested?.Invoke(this); + private bool CanSubmitForReview() => IsTaskBased; + + [RelayCommand(CanExecute = nameof(CanSubmitForReview))] + private void SubmitForReview() + { + if (TaskId is { } id) SubmitForReviewRequested?.Invoke(id); + } + public void Dispose() { Terminal.PropertyChanged -= OnTerminalPropertyChanged; diff --git a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs index b7a3966e..6ed5ee7d 100644 --- a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs @@ -248,6 +248,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable var pane = new ConPtyPaneViewModel(taskId, title, descriptor); pane.ErrorReported += OnConPtyPaneError; pane.CloseRequested += CloseConPtySession; + pane.SubmitForReviewRequested += OnPaneSubmitForReview; ConPtySessions.Add(pane); } catch (Exception ex) @@ -287,6 +288,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable var pane = new ConPtyPaneViewModel(taskId, title, descriptor); pane.ErrorReported += OnConPtyPaneError; pane.CloseRequested += CloseConPtySession; + pane.SubmitForReviewRequested += OnPaneSubmitForReview; ConPtySessions.Add(pane); } catch (Exception ex) @@ -311,6 +313,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable var pane = ConPtyPaneViewModel.CreateAdHoc(title, descriptor); pane.ErrorReported += OnConPtyPaneError; pane.CloseRequested += CloseConPtySession; + pane.SubmitForReviewRequested += OnPaneSubmitForReview; ConPtySessions.Add(pane); } catch (Exception ex) @@ -321,6 +324,22 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable private void OnConPtyPaneError(string message) => ErrorReported?.Invoke(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. + private async void OnPaneSubmitForReview(string taskId) + { + try + { + await _worker.SubmitTaskForReviewAsync(taskId); + if (ConPtySessions.FirstOrDefault(s => s.TaskId == taskId) is { } pane) + CloseConPtySession(pane); + } + catch (Exception ex) + { + ErrorReported?.Invoke(Loc.T("missionControl.submitForReviewFailed", ex.Message)); + } + } + private void CloseConPtySession(ConPtyPaneViewModel pane) { if (!ConPtySessions.Contains(pane)) return; diff --git a/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml b/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml index 2eb3bbdf..0110347a 100644 --- a/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml @@ -372,6 +372,17 @@ ToolTip.Tip="{loc:Tr session.reviewResetTip}" Command="{Binding ResetReviewCommand}" /> + + + + + +