feat(ui): surface agent roadblocks and run outcome in the detail pane
- Parse CLAUDEDO_BLOCKED roadblocks out of the run result and show them in a colored card between Details and Output (ApplyOutcome / ShowRoadblockCard). - Show the run outcome summary as an OUTCOME card in the Output tab, loaded from the task result (falls back to the run's ErrorMarkdown) and refreshed on finish. - Guard the Session tab so it only appears when there are child outcomes. - Make console resize per-task and proportional (description capped at 2/3, console floored at ~1/3) so a long description no longer spills over the footer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<ClaudeDoDbContext> 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();
|
||||
|
||||
Reference in New Issue
Block a user