diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index 5d92c91..d674fbb 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -62,14 +62,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase partial void OnIsNotesModeChanged(bool value) => OnPropertyChanged(nameof(IsTaskDetailVisible)); partial void OnIsPrepModeChanged(bool value) => OnPropertyChanged(nameof(IsTaskDetailVisible)); - // Console maximize: green dot shrinks the description row to its MinHeight so - // the WorkConsole fills the rest. The row stays draggable and never overlaps. - // Applied in DetailsIslandView code-behind (RowDefinitions can't bind). - [ObservableProperty] private bool _isConsoleMaximized; - - [RelayCommand] - private void ToggleConsoleMaximized() => IsConsoleMaximized = !IsConsoleMaximized; - public NotesEditorViewModel Notes { get; private set; } = null!; // Current task row (set by IslandsShellViewModel via Bind) @@ -161,14 +153,15 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase public bool ShowMergeSection => WorktreePath != null || Task?.IsPlanningParent == true || HasChildOutcomes; - // Nothing to manage yet (idle/queued/running standalone): show a hint. - public bool ShowSessionEmpty => - !IsWaitingForReview && !ShowMergeSection && !HasChildOutcomes; - private void NotifySessionSections() { + OnPropertyChanged(nameof(HasChildOutcomes)); OnPropertyChanged(nameof(ShowMergeSection)); - OnPropertyChanged(nameof(ShowSessionEmpty)); + + // The Session tab is only visible when it has outcomes; if it just + // emptied while selected, fall back to Output so the body isn't blank. + if (!HasChildOutcomes && SelectedTab == "session") + SelectedTab = "output"; } public string TurnsText => $"{Turns}/{EffectiveMaxTurns}"; @@ -184,6 +177,53 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase IsFailed ? "The session ended with an error." : IsCancelled ? "The session was cancelled." : ""; + // The session's outcome summary — the task's Result minus any roadblock + // section (those get their own card), falling back to the run's + // ErrorMarkdown for hard failures. Shown once a run has finished. + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowSessionOutcome))] + private string? _sessionOutcome; + + public bool ShowSessionOutcome => + !string.IsNullOrWhiteSpace(SessionOutcome) + && (IsWaitingForReview || IsDone || IsFailed || IsCancelled); + + // The roadblocks the agent emitted (CLAUDEDO_BLOCKED), parsed out of the + // run result so they can surface as a distinct colored card. + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowRoadblockCard))] + private string? _roadblocks; + + public bool ShowRoadblockCard => + !string.IsNullOrWhiteSpace(Roadblocks) + && (IsWaitingForReview || IsDone || IsFailed || IsCancelled); + + // Worker writes roadblocks into the result under this header + // (TaskRunner.ComposeReviewResult). Split it back out for display. + private const string RoadblockMarker = "Roadblocks reported during the run:"; + + private void ApplyOutcome(string? result, string? errorFallback) + { + if (string.IsNullOrWhiteSpace(result)) + { + SessionOutcome = errorFallback; + Roadblocks = null; + return; + } + + var idx = result.IndexOf(RoadblockMarker, StringComparison.Ordinal); + if (idx < 0) + { + SessionOutcome = result; + Roadblocks = null; + return; + } + + var summary = result[..idx].TrimEnd().TrimEnd('⚠').TrimEnd(); + SessionOutcome = string.IsNullOrWhiteSpace(summary) ? null : summary; + Roadblocks = result[(idx + RoadblockMarker.Length)..].Trim(); + } + public string SessionLabel => "claude-session"; // Short task-id badge, e.g. "#T1A" @@ -230,6 +270,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase OnPropertyChanged(nameof(IsAgentSectionEnabled)); OnPropertyChanged(nameof(ShowRoadblock)); OnPropertyChanged(nameof(RoadblockMessage)); + OnPropertyChanged(nameof(ShowSessionOutcome)); + OnPropertyChanged(nameof(ShowRoadblockCard)); NotifySessionSections(); } [ObservableProperty] private string? _model; @@ -364,9 +406,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase public bool ShowMergePreviewMuted => !MergeIsClean && !MergeIsConflict && !string.IsNullOrEmpty(MergePreviewText); - public bool ShowSingleMerge => - WorktreePath != null && Task?.IsPlanningParent != true; - // Claude CLI stream-json parser + buffer for partial text deltas private readonly StreamLineFormatter _formatter = new(); private readonly StringBuilder _claudeBuf = new(); @@ -439,6 +478,21 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase catch { } } + // Reload the session outcome (task Result incl. roadblocks, or the run's + // error) so it appears as soon as a run finishes. + private async System.Threading.Tasks.Task RefreshOutcomeAsync(string taskId) + { + try + { + await using var ctx = await _dbFactory.CreateDbContextAsync(); + var entity = await ctx.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId); + var latestRun = await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId); + if (Task?.Id != taskId) return; + ApplyOutcome(entity?.Result, latestRun?.ErrorMarkdown); + } + catch { } + } + public DetailsIslandViewModel(IDbContextFactory dbFactory, IWorkerClient worker, IServiceProvider services, INotesApi notesApi) { _dbFactory = dbFactory; @@ -492,6 +546,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase // Re-query to pick up worktree created during the run. _ = RefreshWorktreeAsync(taskId); _ = RefreshChildOutcomeAsync(taskId); + _ = RefreshOutcomeAsync(taskId); }; _worker.WorktreeUpdatedEvent += taskId => @@ -755,6 +810,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase CanMergeAll = false; MergeAllDisabledReason = null; MergeAllError = null; + SessionOutcome = null; + Roadblocks = null; _claudeBuf.Clear(); if (row == null) @@ -824,6 +881,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase var latestRun = await runRepo.GetLatestByTaskIdAsync(row.Id, ct); ct.ThrowIfCancellationRequested(); LatestRunSessionId = latestRun?.SessionId; + ApplyOutcome(entity.Result, latestRun?.ErrorMarkdown); // Subscribe only after DB load confirms the task exists _subscribedTaskId = row.Id; @@ -1170,34 +1228,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase MergePreviewText = text; MergeIsClean = clean; MergeIsConflict = conflict; } - [RelayCommand] - private async System.Threading.Tasks.Task MergeAsync() - { - if (Task is null || WorktreePath is null || !_worker.IsConnected) return; - try - { - var result = await _worker.MergeTaskAsync(Task.Id, SelectedMergeTarget ?? "", false, "Merge task"); - if (result.Status == "conflict") - { - if (RequestConflictResolution is not null) - { - await RequestConflictResolution(Task.Id, SelectedMergeTarget ?? ""); - } - else - { - var (text, _, _) = MergePreviewPresenter.Describe( - new MergePreviewDto("conflict", result.ConflictFiles, 0)); - MergePreviewText = text; MergeIsClean = false; MergeIsConflict = true; - } - } - else - { - await RefreshMergePreviewAsync(); - } - } - catch { /* broadcast reconciles */ } - } - [RelayCommand(CanExecute = nameof(CanOpenDiff))] private async System.Threading.Tasks.Task OpenDiffAsync() { @@ -1244,7 +1274,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase OpenDiffCommand.NotifyCanExecuteChanged(); OpenWorktreeCommand.NotifyCanExecuteChanged(); NotifySessionSections(); - OnPropertyChanged(nameof(ShowSingleMerge)); } partial void OnTaskChanged(TaskRowViewModel? value) => NotifySessionSections(); diff --git a/src/ClaudeDo.Ui/Views/Islands/Detail/DescriptionStepsCard.axaml b/src/ClaudeDo.Ui/Views/Islands/Detail/DescriptionStepsCard.axaml index ee30847..76f69e7 100644 --- a/src/ClaudeDo.Ui/Views/Islands/Detail/DescriptionStepsCard.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/Detail/DescriptionStepsCard.axaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands" xmlns:ctl="using:ClaudeDo.Ui.Views.Controls" + xmlns:loc="using:ClaudeDo.Ui.Localization" x:Class="ClaudeDo.Ui.Views.Islands.Detail.DescriptionStepsCard" x:DataType="vm:DetailsIslandViewModel"> @@ -21,7 +22,7 @@ @@ -30,6 +31,7 @@ + @@ -183,6 +173,7 @@