chore(claude-do): refactor(ui): DetailsIslandViewModel (1431 Zeilen) in Sektio
Kontext: src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs ist mit 1431 Zeilen ein God-VM mit ~12 Concerns (Log-Streaming, Titel/Description-Editing, Subtasks, Child-Outcomes, Merge-Preview/-Targets, Diff, Agent-Settings-Overrides, Notes-Mode, Prep-Mode, Tabs, Session-Outcome/Roadblocks, Worktree-Info). Jedes neue Feature landet dort. Änderungen — drei klar abgrenzbare Sektionen als ei ClaudeDo-Task: 483e419f-1ec8-46ba-986b-8b90d6596b49
This commit is contained in:
@@ -0,0 +1,196 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using ClaudeDo.Ui.Helpers;
|
||||||
|
using ClaudeDo.Ui.Localization;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
|
public sealed partial class AgentSettingsSectionViewModel : ViewModelBase, IDisposable
|
||||||
|
{
|
||||||
|
private readonly IWorkerClient _worker;
|
||||||
|
private readonly EventHandler _langChangedHandler;
|
||||||
|
|
||||||
|
internal string? TaskId { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(IsAgentSectionEnabled))]
|
||||||
|
private bool _isRunning;
|
||||||
|
|
||||||
|
public bool IsAgentSectionEnabled => !IsRunning;
|
||||||
|
|
||||||
|
[ObservableProperty] private string? _taskModelSelection;
|
||||||
|
[ObservableProperty] private string _taskSystemPrompt = "";
|
||||||
|
[ObservableProperty] private AgentInfo? _taskSelectedAgent;
|
||||||
|
[ObservableProperty] private decimal? _taskMaxTurns;
|
||||||
|
[ObservableProperty] private string _modelBadge = "";
|
||||||
|
[ObservableProperty] private string _modelInheritedHint = "";
|
||||||
|
[ObservableProperty] private string _turnsBadge = "";
|
||||||
|
[ObservableProperty] private string _turnsInheritedHint = "";
|
||||||
|
[ObservableProperty] private string _agentBadge = "";
|
||||||
|
[ObservableProperty] private string _effectiveSystemPromptHint = "";
|
||||||
|
|
||||||
|
private string _globalModel = ModelRegistry.DefaultAlias;
|
||||||
|
private int _globalMaxTurns = 100;
|
||||||
|
private string? _listModel;
|
||||||
|
private int? _listMaxTurns;
|
||||||
|
private string? _listAgentName;
|
||||||
|
|
||||||
|
private bool _suppressAgentSave;
|
||||||
|
private CancellationTokenSource? _agentSaveCts;
|
||||||
|
|
||||||
|
public int EffectiveMaxTurns =>
|
||||||
|
TaskMaxTurns is decimal t ? (int)t : (_listMaxTurns ?? _globalMaxTurns);
|
||||||
|
|
||||||
|
public ObservableCollection<string> TaskModelOptions { get; } = new(ModelRegistry.Aliases);
|
||||||
|
public ObservableCollection<AgentInfo> TaskAgentOptions { get; } = new();
|
||||||
|
|
||||||
|
public AgentSettingsSectionViewModel(IWorkerClient worker)
|
||||||
|
{
|
||||||
|
_worker = worker;
|
||||||
|
_langChangedHandler = (_, _) =>
|
||||||
|
{
|
||||||
|
RecomputeModelBadge();
|
||||||
|
RecomputeTurnsBadge();
|
||||||
|
RecomputeAgentBadge();
|
||||||
|
};
|
||||||
|
Loc.LanguageChanged += _langChangedHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => Loc.LanguageChanged -= _langChangedHandler;
|
||||||
|
|
||||||
|
partial void OnTaskModelSelectionChanged(string? value) { RecomputeModelBadge(); QueueAgentSave(); }
|
||||||
|
|
||||||
|
partial void OnTaskMaxTurnsChanged(decimal? value)
|
||||||
|
{
|
||||||
|
RecomputeTurnsBadge();
|
||||||
|
OnPropertyChanged(nameof(EffectiveMaxTurns));
|
||||||
|
QueueAgentSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnTaskSystemPromptChanged(string value) => QueueAgentSave();
|
||||||
|
partial void OnTaskSelectedAgentChanged(AgentInfo? value) { RecomputeAgentBadge(); QueueAgentSave(); }
|
||||||
|
|
||||||
|
private void RecomputeModelBadge()
|
||||||
|
{
|
||||||
|
var (value, source) = InheritanceResolver.Resolve(TaskModelSelection, _listModel, _globalModel);
|
||||||
|
ModelInheritedHint = value;
|
||||||
|
ModelBadge = BadgeFor(source, !string.IsNullOrWhiteSpace(TaskModelSelection));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecomputeTurnsBadge()
|
||||||
|
{
|
||||||
|
var (value, source) = InheritanceResolver.Resolve(
|
||||||
|
TaskMaxTurns?.ToString(), _listMaxTurns?.ToString(), _globalMaxTurns.ToString());
|
||||||
|
TurnsInheritedHint = value;
|
||||||
|
TurnsBadge = BadgeFor(source, TaskMaxTurns is not null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecomputeAgentBadge()
|
||||||
|
{
|
||||||
|
var taskSet = TaskSelectedAgent is not null && !string.IsNullOrWhiteSpace(TaskSelectedAgent.Path);
|
||||||
|
var (_, source) = InheritanceResolver.Resolve(
|
||||||
|
taskSet ? TaskSelectedAgent!.Path : null, _listAgentName, null);
|
||||||
|
AgentBadge = BadgeFor(source, taskSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BadgeFor(InheritSource source, bool taskSet) => taskSet
|
||||||
|
? Loc.T("settings.inherit.overrideBadge")
|
||||||
|
: source == InheritSource.List
|
||||||
|
? Loc.T("settings.inherit.inheritedFromList")
|
||||||
|
: Loc.T("settings.inherit.inheritedFromGlobal");
|
||||||
|
|
||||||
|
private void QueueAgentSave()
|
||||||
|
{
|
||||||
|
if (_suppressAgentSave || TaskId is null) return;
|
||||||
|
_agentSaveCts?.Cancel();
|
||||||
|
_agentSaveCts = new CancellationTokenSource();
|
||||||
|
_ = SaveAgentSettingsAsync(_agentSaveCts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async System.Threading.Tasks.Task SaveAgentSettingsAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await System.Threading.Tasks.Task.Delay(300, ct);
|
||||||
|
if (TaskId is null) return;
|
||||||
|
|
||||||
|
var model = string.IsNullOrWhiteSpace(TaskModelSelection) ? null : TaskModelSelection;
|
||||||
|
var sp = string.IsNullOrWhiteSpace(TaskSystemPrompt) ? null : TaskSystemPrompt;
|
||||||
|
var ap = TaskSelectedAgent is null || string.IsNullOrWhiteSpace(TaskSelectedAgent.Path)
|
||||||
|
? null : TaskSelectedAgent.Path;
|
||||||
|
var turns = TaskMaxTurns is decimal d ? (int?)d : null;
|
||||||
|
|
||||||
|
await _worker.UpdateTaskAgentSettingsAsync(
|
||||||
|
new UpdateTaskAgentSettingsDto(TaskId, model, sp, ap, turns));
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async System.Threading.Tasks.Task LoadAsync(
|
||||||
|
ClaudeDo.Data.Models.TaskEntity entity, CancellationToken ct)
|
||||||
|
{
|
||||||
|
_suppressAgentSave = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TaskAgentOptions.Clear();
|
||||||
|
TaskAgentOptions.Add(new AgentInfo("(inherited)", "", ""));
|
||||||
|
var agents = await _worker.GetAgentsAsync();
|
||||||
|
foreach (var a in agents) TaskAgentOptions.Add(a);
|
||||||
|
|
||||||
|
TaskModelSelection = string.IsNullOrWhiteSpace(entity.Model) ? null : entity.Model!;
|
||||||
|
TaskMaxTurns = entity.MaxTurns is int tmt ? tmt : (decimal?)null;
|
||||||
|
TaskSystemPrompt = entity.SystemPrompt ?? "";
|
||||||
|
TaskSelectedAgent = string.IsNullOrWhiteSpace(entity.AgentPath)
|
||||||
|
? TaskAgentOptions[0]
|
||||||
|
: (TaskAgentOptions.FirstOrDefault(a => a.Path == entity.AgentPath) ?? TaskAgentOptions[0]);
|
||||||
|
|
||||||
|
var listCfg = await _worker.GetListConfigAsync(entity.ListId);
|
||||||
|
var app = await _worker.GetAppSettingsAsync();
|
||||||
|
_globalModel = app?.DefaultModel ?? ModelRegistry.DefaultAlias;
|
||||||
|
_globalMaxTurns = app?.DefaultMaxTurns ?? 100;
|
||||||
|
_listModel = listCfg?.Model;
|
||||||
|
_listMaxTurns = listCfg?.MaxTurns;
|
||||||
|
_listAgentName = string.IsNullOrWhiteSpace(listCfg?.AgentPath)
|
||||||
|
? null : System.IO.Path.GetFileName(listCfg!.AgentPath!);
|
||||||
|
|
||||||
|
EffectiveSystemPromptHint = string.IsNullOrWhiteSpace(listCfg?.SystemPrompt)
|
||||||
|
? "" : listCfg!.SystemPrompt!;
|
||||||
|
|
||||||
|
RecomputeModelBadge();
|
||||||
|
RecomputeTurnsBadge();
|
||||||
|
RecomputeAgentBadge();
|
||||||
|
OnPropertyChanged(nameof(EffectiveMaxTurns));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_suppressAgentSave = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Clear()
|
||||||
|
{
|
||||||
|
_suppressAgentSave = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TaskModelSelection = null;
|
||||||
|
TaskMaxTurns = null;
|
||||||
|
TaskSystemPrompt = "";
|
||||||
|
TaskSelectedAgent = null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_suppressAgentSave = false;
|
||||||
|
}
|
||||||
|
EffectiveSystemPromptHint = "";
|
||||||
|
TaskId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand] private void ResetTaskModel() => TaskModelSelection = null;
|
||||||
|
[RelayCommand] private void ResetTaskTurns() => TaskMaxTurns = null;
|
||||||
|
[RelayCommand] private void ResetTaskAgent() =>
|
||||||
|
TaskSelectedAgent = TaskAgentOptions.Count > 0 ? TaskAgentOptions[0] : null;
|
||||||
|
}
|
||||||
@@ -53,6 +53,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
private readonly IServiceProvider _services;
|
private readonly IServiceProvider _services;
|
||||||
private readonly INotesApi _notesApi;
|
private readonly INotesApi _notesApi;
|
||||||
|
|
||||||
|
// ── Section view models ───────────────────────────────────────────────────
|
||||||
|
public AgentSettingsSectionViewModel AgentSettings { get; }
|
||||||
|
public MergeSectionViewModel Merge { get; }
|
||||||
|
public PrepPanelViewModel Prep { get; }
|
||||||
|
|
||||||
// Captured handler delegates for disposal
|
// Captured handler delegates for disposal
|
||||||
private readonly EventHandler _langChangedHandler;
|
private readonly EventHandler _langChangedHandler;
|
||||||
private readonly System.ComponentModel.PropertyChangedEventHandler _workerPropertyChangedHandler;
|
private readonly System.ComponentModel.PropertyChangedEventHandler _workerPropertyChangedHandler;
|
||||||
@@ -63,8 +68,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
[ObservableProperty] private bool _isNotesMode;
|
[ObservableProperty] private bool _isNotesMode;
|
||||||
[ObservableProperty] private bool _isPrepMode;
|
[ObservableProperty] private bool _isPrepMode;
|
||||||
[ObservableProperty] private bool _isPrepRunning;
|
|
||||||
public ObservableCollection<LogLineViewModel> PrepLog { get; } = new();
|
|
||||||
public bool IsTaskDetailVisible => !IsNotesMode && !IsPrepMode;
|
public bool IsTaskDetailVisible => !IsNotesMode && !IsPrepMode;
|
||||||
|
|
||||||
partial void OnIsNotesModeChanged(bool value) => OnPropertyChanged(nameof(IsTaskDetailVisible));
|
partial void OnIsNotesModeChanged(bool value) => OnPropertyChanged(nameof(IsTaskDetailVisible));
|
||||||
@@ -77,7 +81,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
[NotifyCanExecuteChangedFor(nameof(EnqueueCommand))]
|
[NotifyCanExecuteChangedFor(nameof(EnqueueCommand))]
|
||||||
[NotifyCanExecuteChangedFor(nameof(DequeueCommand))]
|
[NotifyCanExecuteChangedFor(nameof(DequeueCommand))]
|
||||||
[NotifyCanExecuteChangedFor(nameof(ResetAndRetryCommand))]
|
[NotifyCanExecuteChangedFor(nameof(ResetAndRetryCommand))]
|
||||||
[NotifyCanExecuteChangedFor(nameof(ReviewCombinedDiffCommand))]
|
|
||||||
[NotifyPropertyChangedFor(nameof(TaskIdBadge))]
|
[NotifyPropertyChangedFor(nameof(TaskIdBadge))]
|
||||||
private TaskRowViewModel? _task;
|
private TaskRowViewModel? _task;
|
||||||
|
|
||||||
@@ -112,9 +115,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ToggleDescriptionExpanded() => IsDescriptionExpanded = !IsDescriptionExpanded;
|
private void ToggleDescriptionExpanded() => IsDescriptionExpanded = !IsDescriptionExpanded;
|
||||||
|
|
||||||
// ── Description / Steps card (redesign) ─────────────────────────────
|
|
||||||
// Description is always the card body; steps live in an expandable summary
|
|
||||||
// strip below it so step presence is visible without switching views.
|
|
||||||
[ObservableProperty] private bool _isStepsExpanded;
|
[ObservableProperty] private bool _isStepsExpanded;
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -135,14 +135,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
OnPropertyChanged(nameof(ComposedPreview));
|
OnPropertyChanged(nameof(ComposedPreview));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The exact text handed to Claude: title + description + open steps only.
|
|
||||||
public string ComposedPreview =>
|
public string ComposedPreview =>
|
||||||
ClaudeDo.Data.TaskPromptComposer.Compose(
|
ClaudeDo.Data.TaskPromptComposer.Compose(
|
||||||
EditableTitle, EditableDescription, Subtasks.Select(s => (s.Title, s.Done)));
|
EditableTitle, EditableDescription, Subtasks.Select(s => (s.Title, s.Done)));
|
||||||
|
|
||||||
// ── Work console (redesign) ────────────────────────────────────────
|
|
||||||
// Two tabs: Output (live log) and Session (review + merge/worktree +
|
|
||||||
// outcomes, each section gated on the relevant state).
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(IsOutputTab))]
|
[NotifyPropertyChangedFor(nameof(IsOutputTab))]
|
||||||
[NotifyPropertyChangedFor(nameof(IsGitTab))]
|
[NotifyPropertyChangedFor(nameof(IsGitTab))]
|
||||||
@@ -156,39 +152,25 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void SelectTab(string? tab) => SelectedTab = tab ?? "output";
|
private void SelectTab(string? tab) => SelectedTab = tab ?? "output";
|
||||||
|
|
||||||
// Merge/worktree controls only matter once there's a worktree to manage
|
|
||||||
// (standalone task), or a planning parent / improvement parent with children.
|
|
||||||
public bool ShowMergeSection =>
|
|
||||||
WorktreePath != null || Task?.IsPlanningParent == true || HasChildOutcomes;
|
|
||||||
|
|
||||||
private void NotifySessionSections()
|
private void NotifySessionSections()
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(HasChildOutcomes));
|
OnPropertyChanged(nameof(HasChildOutcomes));
|
||||||
OnPropertyChanged(nameof(ShowMergeSection));
|
Merge.SyncChildOutcomes(HasChildOutcomes, Subtasks.Count);
|
||||||
NotifyAttention();
|
NotifyAttention();
|
||||||
|
|
||||||
// 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")
|
if (!HasChildOutcomes && SelectedTab == "session")
|
||||||
SelectedTab = "output";
|
SelectedTab = "output";
|
||||||
}
|
}
|
||||||
|
|
||||||
public string TurnsText => $"{Turns}/{EffectiveMaxTurns}";
|
public string TurnsText => $"{Turns}/{AgentSettings.EffectiveMaxTurns}";
|
||||||
public string DiffAddText => $"+{DiffAdditions}";
|
public string DiffAddText => $"+{DiffAdditions}";
|
||||||
public string DiffDelText => $"-{DiffDeletions}";
|
public string DiffDelText => $"-{DiffDeletions}";
|
||||||
|
|
||||||
// Resolved turn budget: per-task override → list default → global default.
|
|
||||||
public int EffectiveMaxTurns =>
|
|
||||||
TaskMaxTurns is decimal t ? (int)t : (_listMaxTurns ?? _globalMaxTurns);
|
|
||||||
|
|
||||||
public bool ShowRoadblock => IsFailed || IsCancelled;
|
public bool ShowRoadblock => IsFailed || IsCancelled;
|
||||||
public string RoadblockMessage =>
|
public string RoadblockMessage =>
|
||||||
IsFailed ? "The session ended with an error." :
|
IsFailed ? "The session ended with an error." :
|
||||||
IsCancelled ? "The session was cancelled." : "";
|
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]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(ShowSessionOutcome))]
|
[NotifyPropertyChangedFor(nameof(ShowSessionOutcome))]
|
||||||
private string? _sessionOutcome;
|
private string? _sessionOutcome;
|
||||||
@@ -197,8 +179,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
!string.IsNullOrWhiteSpace(SessionOutcome)
|
!string.IsNullOrWhiteSpace(SessionOutcome)
|
||||||
&& (IsWaitingForReview || IsDone || IsFailed || IsCancelled);
|
&& (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]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(ShowRoadblockCard))]
|
[NotifyPropertyChangedFor(nameof(ShowRoadblockCard))]
|
||||||
private string? _roadblocks;
|
private string? _roadblocks;
|
||||||
@@ -207,8 +187,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
!string.IsNullOrWhiteSpace(Roadblocks)
|
!string.IsNullOrWhiteSpace(Roadblocks)
|
||||||
&& (IsWaitingForReview || IsDone || IsFailed || IsCancelled);
|
&& (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 const string RoadblockMarker = "Roadblocks reported during the run:";
|
||||||
|
|
||||||
private void ApplyOutcome(string? result, string? errorFallback)
|
private void ApplyOutcome(string? result, string? errorFallback)
|
||||||
@@ -235,7 +213,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
public string SessionLabel => "claude-session";
|
public string SessionLabel => "claude-session";
|
||||||
|
|
||||||
// Short task-id badge, e.g. "#T1A"
|
|
||||||
public string TaskIdBadge => Task != null ? $"#T{Task.Id[..Math.Min(3, Task.Id.Length)].ToUpperInvariant()}" : "";
|
public string TaskIdBadge => Task != null ? $"#T{Task.Id[..Math.Min(3, Task.Id.Length)].ToUpperInvariant()}" : "";
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -251,12 +228,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
public bool IsWaitingForReview => AgentState == "review";
|
public bool IsWaitingForReview => AgentState == "review";
|
||||||
public bool IsWaitingForChildren => AgentState == "children";
|
public bool IsWaitingForChildren => AgentState == "children";
|
||||||
public bool IsDone => AgentState == "done";
|
public bool IsDone => AgentState == "done";
|
||||||
public bool IsFailed => AgentState == "failed";
|
public bool IsFailed => AgentState == "failed";
|
||||||
public bool IsCancelled => AgentState == "cancelled";
|
public bool IsCancelled => AgentState == "cancelled";
|
||||||
|
|
||||||
// Recovery actions: Continue (resume session) for Failed/Cancelled.
|
|
||||||
public bool ShowContinue => IsFailed || IsCancelled;
|
public bool ShowContinue => IsFailed || IsCancelled;
|
||||||
// Reset & retry available from any terminal state.
|
|
||||||
public bool ShowResetAndRetry => IsFailed || IsCancelled || IsDone;
|
public bool ShowResetAndRetry => IsFailed || IsCancelled || IsDone;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -276,89 +251,20 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
OnPropertyChanged(nameof(IsCancelled));
|
OnPropertyChanged(nameof(IsCancelled));
|
||||||
OnPropertyChanged(nameof(ShowContinue));
|
OnPropertyChanged(nameof(ShowContinue));
|
||||||
OnPropertyChanged(nameof(ShowResetAndRetry));
|
OnPropertyChanged(nameof(ShowResetAndRetry));
|
||||||
OnPropertyChanged(nameof(IsAgentSectionEnabled));
|
|
||||||
OnPropertyChanged(nameof(ShowRoadblock));
|
OnPropertyChanged(nameof(ShowRoadblock));
|
||||||
OnPropertyChanged(nameof(RoadblockMessage));
|
OnPropertyChanged(nameof(RoadblockMessage));
|
||||||
OnPropertyChanged(nameof(ShowSessionOutcome));
|
OnPropertyChanged(nameof(ShowSessionOutcome));
|
||||||
OnPropertyChanged(nameof(ShowRoadblockCard));
|
OnPropertyChanged(nameof(ShowRoadblockCard));
|
||||||
|
AgentSettings.IsRunning = IsRunning;
|
||||||
NotifySessionSections();
|
NotifySessionSections();
|
||||||
}
|
}
|
||||||
|
|
||||||
[ObservableProperty] private string? _model;
|
[ObservableProperty] private string? _model;
|
||||||
|
|
||||||
// Agent settings overrides
|
|
||||||
[ObservableProperty] private string? _taskModelSelection; // null = inherit
|
|
||||||
[ObservableProperty] private string _taskSystemPrompt = "";
|
|
||||||
[ObservableProperty] private AgentInfo? _taskSelectedAgent;
|
|
||||||
[ObservableProperty] private decimal? _taskMaxTurns; // null = inherit
|
|
||||||
[ObservableProperty] private string _modelBadge = "";
|
|
||||||
[ObservableProperty] private string _modelInheritedHint = "";
|
|
||||||
[ObservableProperty] private string _turnsBadge = "";
|
|
||||||
[ObservableProperty] private string _turnsInheritedHint = "";
|
|
||||||
[ObservableProperty] private string _agentBadge = "";
|
|
||||||
[ObservableProperty] private string _effectiveSystemPromptHint = "";
|
|
||||||
|
|
||||||
private string _globalModel = ModelRegistry.DefaultAlias;
|
|
||||||
private int _globalMaxTurns = 100;
|
|
||||||
private string? _listModel;
|
|
||||||
private int? _listMaxTurns;
|
|
||||||
private string? _listAgentName;
|
|
||||||
|
|
||||||
partial void OnTaskModelSelectionChanged(string? value) { RecomputeModelBadge(); QueueAgentSave(); }
|
|
||||||
partial void OnTaskMaxTurnsChanged(decimal? value)
|
|
||||||
{
|
|
||||||
RecomputeTurnsBadge();
|
|
||||||
OnPropertyChanged(nameof(EffectiveMaxTurns));
|
|
||||||
OnPropertyChanged(nameof(TurnsText));
|
|
||||||
QueueAgentSave();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RecomputeModelBadge()
|
|
||||||
{
|
|
||||||
var (value, source) = InheritanceResolver.Resolve(TaskModelSelection, _listModel, _globalModel);
|
|
||||||
ModelInheritedHint = value;
|
|
||||||
ModelBadge = BadgeFor(source, !string.IsNullOrWhiteSpace(TaskModelSelection));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RecomputeTurnsBadge()
|
|
||||||
{
|
|
||||||
var (value, source) = InheritanceResolver.Resolve(
|
|
||||||
TaskMaxTurns?.ToString(), _listMaxTurns?.ToString(), _globalMaxTurns.ToString());
|
|
||||||
TurnsInheritedHint = value;
|
|
||||||
TurnsBadge = BadgeFor(source, TaskMaxTurns is not null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RecomputeAgentBadge()
|
|
||||||
{
|
|
||||||
var taskSet = TaskSelectedAgent is not null && !string.IsNullOrWhiteSpace(TaskSelectedAgent.Path);
|
|
||||||
var (_, source) = InheritanceResolver.Resolve(
|
|
||||||
taskSet ? TaskSelectedAgent!.Path : null, _listAgentName, null);
|
|
||||||
AgentBadge = BadgeFor(source, taskSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BadgeFor(InheritSource source, bool taskSet) => taskSet
|
|
||||||
? Loc.T("settings.inherit.overrideBadge")
|
|
||||||
: source == InheritSource.List
|
|
||||||
? Loc.T("settings.inherit.inheritedFromList")
|
|
||||||
: Loc.T("settings.inherit.inheritedFromGlobal");
|
|
||||||
|
|
||||||
public System.Collections.ObjectModel.ObservableCollection<string> TaskModelOptions { get; } = new(ModelRegistry.Aliases);
|
|
||||||
|
|
||||||
public System.Collections.ObjectModel.ObservableCollection<AgentInfo> TaskAgentOptions { get; } = new();
|
|
||||||
|
|
||||||
private bool _suppressAgentSave;
|
|
||||||
private CancellationTokenSource? _agentSaveCts;
|
|
||||||
|
|
||||||
private bool _suppressDescSave;
|
|
||||||
private CancellationTokenSource? _descSaveCts;
|
|
||||||
|
|
||||||
public bool IsAgentSectionEnabled => !IsRunning;
|
|
||||||
|
|
||||||
[ObservableProperty] private string? _worktreePath;
|
[ObservableProperty] private string? _worktreePath;
|
||||||
[ObservableProperty] private string? _worktreeBaseCommit;
|
[ObservableProperty] private string? _worktreeBaseCommit;
|
||||||
[ObservableProperty] private string? _worktreeHeadCommit;
|
[ObservableProperty] private string? _worktreeHeadCommit;
|
||||||
[ObservableProperty] private string? _worktreeStateLabel;
|
[ObservableProperty] private string? _worktreeStateLabel;
|
||||||
// Repo working dir of the selected task's list — used to diff a merged task's
|
|
||||||
// commit range after its worktree directory is gone.
|
|
||||||
private string? _listWorkingDir;
|
private string? _listWorkingDir;
|
||||||
[ObservableProperty] private string? _branchLine;
|
[ObservableProperty] private string? _branchLine;
|
||||||
[ObservableProperty] private int _turns;
|
[ObservableProperty] private int _turns;
|
||||||
@@ -368,14 +274,13 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
[ObservableProperty] private int _commitsOnBranch;
|
[ObservableProperty] private int _commitsOnBranch;
|
||||||
|
|
||||||
public string TokensFormatted => Tokens >= 1000 ? $"{Tokens / 1000.0:F1}k" : Tokens.ToString();
|
public string TokensFormatted => Tokens >= 1000 ? $"{Tokens / 1000.0:F1}k" : Tokens.ToString();
|
||||||
public string ElapsedFormatted => ""; // placeholder — no start-time stored yet
|
public string ElapsedFormatted => "";
|
||||||
|
|
||||||
partial void OnTokensChanged(int value) => OnPropertyChanged(nameof(TokensFormatted));
|
partial void OnTokensChanged(int value) => OnPropertyChanged(nameof(TokensFormatted));
|
||||||
partial void OnTurnsChanged(int value) => OnPropertyChanged(nameof(TurnsText));
|
partial void OnTurnsChanged(int value) => OnPropertyChanged(nameof(TurnsText));
|
||||||
partial void OnDiffAdditionsChanged(int value) { OnPropertyChanged(nameof(DiffMeterRatio)); OnPropertyChanged(nameof(DiffAddText)); }
|
partial void OnDiffAdditionsChanged(int value) { OnPropertyChanged(nameof(DiffMeterRatio)); OnPropertyChanged(nameof(DiffAddText)); }
|
||||||
partial void OnDiffDeletionsChanged(int value) { OnPropertyChanged(nameof(DiffMeterRatio)); OnPropertyChanged(nameof(DiffDelText)); }
|
partial void OnDiffDeletionsChanged(int value) { OnPropertyChanged(nameof(DiffMeterRatio)); OnPropertyChanged(nameof(DiffDelText)); }
|
||||||
|
|
||||||
// 0.0–1.0 additions share for the diff meter
|
|
||||||
public double DiffMeterRatio
|
public double DiffMeterRatio
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -387,16 +292,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
public ObservableCollection<LogLineViewModel> Log { get; } = new();
|
public ObservableCollection<LogLineViewModel> Log { get; } = new();
|
||||||
public ObservableCollection<SubtaskRowViewModel> Subtasks { get; } = new();
|
public ObservableCollection<SubtaskRowViewModel> Subtasks { get; } = new();
|
||||||
|
|
||||||
// Agent-suggested improvement children of a non-planning parent, surfaced on its
|
|
||||||
// review card with each child's outcome and rolled-up roadblock count.
|
|
||||||
public ObservableCollection<ChildOutcomeRowViewModel> ChildOutcomes { get; } = new();
|
public ObservableCollection<ChildOutcomeRowViewModel> ChildOutcomes { get; } = new();
|
||||||
public bool HasChildOutcomes => ChildOutcomes.Count > 0;
|
public bool HasChildOutcomes => ChildOutcomes.Count > 0;
|
||||||
|
|
||||||
// Children that need the user's attention before the parent can be approved:
|
|
||||||
// failed, cancelled, still awaiting their own review, or that reported roadblocks.
|
|
||||||
// The parent deliberately stays in WaitingForChildren until these are resolved;
|
|
||||||
// this surfaces a flag so the roadblock is visible on the parent.
|
|
||||||
public int ChildrenNeedingAttention => ChildOutcomes.Count(c =>
|
public int ChildrenNeedingAttention => ChildOutcomes.Count(c =>
|
||||||
c.Status == ClaudeDo.Data.Models.TaskStatus.Failed
|
c.Status == ClaudeDo.Data.Models.TaskStatus.Failed
|
||||||
|| c.Status == ClaudeDo.Data.Models.TaskStatus.Cancelled
|
|| c.Status == ClaudeDo.Data.Models.TaskStatus.Cancelled
|
||||||
@@ -416,59 +314,37 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
[ObservableProperty] private string _newSubtaskTitle = "";
|
[ObservableProperty] private string _newSubtaskTitle = "";
|
||||||
|
|
||||||
// Planning merge controls
|
|
||||||
[ObservableProperty] private ObservableCollection<string> _mergeTargetBranches = new();
|
|
||||||
[ObservableProperty] private string? _selectedMergeTarget;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyPropertyChangedFor(nameof(ShowMergePreviewMuted))]
|
|
||||||
private string _mergePreviewText = "";
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyPropertyChangedFor(nameof(ShowMergePreviewMuted))]
|
|
||||||
private bool _mergeIsClean;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyPropertyChangedFor(nameof(ShowMergePreviewMuted))]
|
|
||||||
private bool _mergeIsConflict;
|
|
||||||
|
|
||||||
public bool ShowMergePreviewMuted =>
|
|
||||||
!MergeIsClean && !MergeIsConflict && !string.IsNullOrEmpty(MergePreviewText);
|
|
||||||
|
|
||||||
// Claude CLI stream-json parser + buffer for partial text deltas
|
// Claude CLI stream-json parser + buffer for partial text deltas
|
||||||
private readonly StreamLineFormatter _formatter = new();
|
private readonly StreamLineFormatter _formatter = new();
|
||||||
private readonly StringBuilder _claudeBuf = new();
|
private readonly StringBuilder _claudeBuf = new();
|
||||||
private readonly StringBuilder _prepClaudeBuf = new();
|
|
||||||
|
|
||||||
// The task ID we are currently subscribed to for live log messages
|
|
||||||
private string? _subscribedTaskId;
|
private string? _subscribedTaskId;
|
||||||
|
|
||||||
private CancellationTokenSource? _loadCts;
|
private CancellationTokenSource? _loadCts;
|
||||||
|
|
||||||
|
private bool _suppressDescSave;
|
||||||
|
private CancellationTokenSource? _descSaveCts;
|
||||||
|
|
||||||
// Set by shell so CloseDetailCommand can clear SelectedTask
|
// Set by shell so CloseDetailCommand can clear SelectedTask
|
||||||
public Action? CloseDetail { get; set; }
|
public Action? CloseDetail { get; set; }
|
||||||
|
|
||||||
// Set by shell so DeleteTaskCommand can remove from list
|
// Set by shell so DeleteTaskCommand can remove from list
|
||||||
public Func<TaskRowViewModel, System.Threading.Tasks.Task>? DeleteFromList { get; set; }
|
public Func<TaskRowViewModel, System.Threading.Tasks.Task>? DeleteFromList { get; set; }
|
||||||
|
|
||||||
// Set by the view so OpenDiffCommand can show the modal as a dialog
|
|
||||||
public Func<DiffModalViewModel, System.Threading.Tasks.Task>? ShowDiffModal { get; set; }
|
|
||||||
|
|
||||||
// Set by the view so OpenDiff can pass through merge requests from the diff modal
|
|
||||||
public Func<MergeModalViewModel, System.Threading.Tasks.Task>? ShowMergeModal { get; set; }
|
|
||||||
|
|
||||||
// Set by the view so DeleteTaskCommand can prompt yes/no before deleting
|
// Set by the view so DeleteTaskCommand can prompt yes/no before deleting
|
||||||
public Func<string, System.Threading.Tasks.Task<bool>>? ConfirmAsync { get; set; }
|
public Func<string, System.Threading.Tasks.Task<bool>>? ConfirmAsync { get; set; }
|
||||||
|
|
||||||
// Set by the view so ReviewCombinedDiffCommand can show the planning diff modal
|
|
||||||
public Func<ViewModels.Planning.PlanningDiffViewModel, System.Threading.Tasks.Task>? ShowPlanningDiffModal { get; set; }
|
|
||||||
|
|
||||||
// Set by the view so DeleteTaskCommand can show an error message
|
// Set by the view so DeleteTaskCommand can show an error message
|
||||||
public Func<string, System.Threading.Tasks.Task>? ShowErrorAsync { get; set; }
|
public Func<string, System.Threading.Tasks.Task>? ShowErrorAsync { get; set; }
|
||||||
|
|
||||||
// Invoked when a single-task merge/approve hits a conflict. Wired by the
|
// ── Review ──────────────────────────────────────────────────────────────
|
||||||
// integrator to Layer C's conflict resolver. Args: (taskId, targetBranch).
|
[ObservableProperty] private string _reviewFeedback = "";
|
||||||
public Func<string, string, System.Threading.Tasks.Task>? RequestConflictResolution { get; set; }
|
|
||||||
|
// Kept for backwards-compat surface — delegates to Merge.RequestConflictResolution
|
||||||
|
public Func<string, string, System.Threading.Tasks.Task>? RequestConflictResolution
|
||||||
|
{
|
||||||
|
get => Merge.RequestConflictResolution;
|
||||||
|
set => Merge.RequestConflictResolution = value;
|
||||||
|
}
|
||||||
|
|
||||||
private static string StatusToStateKey(ClaudeDo.Data.Models.TaskStatus status) => status switch
|
private static string StatusToStateKey(ClaudeDo.Data.Models.TaskStatus status) => status switch
|
||||||
{
|
{
|
||||||
@@ -507,8 +383,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
catch { }
|
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)
|
private async System.Threading.Tasks.Task RefreshOutcomeAsync(string taskId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -522,30 +396,36 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public DetailsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker, IServiceProvider services, INotesApi notesApi)
|
public DetailsIslandViewModel(
|
||||||
|
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||||
|
IWorkerClient worker,
|
||||||
|
IServiceProvider services,
|
||||||
|
INotesApi notesApi)
|
||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_worker = worker;
|
_worker = worker;
|
||||||
_services = services;
|
_services = services;
|
||||||
_notesApi = notesApi;
|
_notesApi = notesApi;
|
||||||
|
|
||||||
|
AgentSettings = new AgentSettingsSectionViewModel(worker);
|
||||||
|
Merge = new MergeSectionViewModel(worker, services);
|
||||||
|
Prep = new PrepPanelViewModel(worker);
|
||||||
|
|
||||||
Notes = new NotesEditorViewModel(_notesApi);
|
Notes = new NotesEditorViewModel(_notesApi);
|
||||||
Subtasks.CollectionChanged += (_, _) => NotifyStepsChanged();
|
Subtasks.CollectionChanged += (_, _) => NotifyStepsChanged();
|
||||||
_langChangedHandler = (_, _) =>
|
Subtasks.CollectionChanged += (_, _) => Merge.SyncChildOutcomes(HasChildOutcomes, Subtasks.Count);
|
||||||
|
|
||||||
|
AgentSettings.PropertyChanged += (_, e) =>
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(AgentStatusLabel));
|
if (e.PropertyName == nameof(AgentSettingsSectionViewModel.EffectiveMaxTurns))
|
||||||
RecomputeModelBadge();
|
OnPropertyChanged(nameof(TurnsText));
|
||||||
RecomputeTurnsBadge();
|
|
||||||
RecomputeAgentBadge();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_langChangedHandler = (_, _) => OnPropertyChanged(nameof(AgentStatusLabel));
|
||||||
Loc.LanguageChanged += _langChangedHandler;
|
Loc.LanguageChanged += _langChangedHandler;
|
||||||
|
|
||||||
// Subscribe once; filter by current task id inside the handler
|
|
||||||
_worker.TaskMessageEvent += OnTaskMessage;
|
_worker.TaskMessageEvent += OnTaskMessage;
|
||||||
_worker.PrepStartedEvent += OnPrepStarted;
|
|
||||||
_worker.PrepLineEvent += OnPrepLine;
|
|
||||||
_worker.PrepFinishedEvent += OnPrepFinished;
|
|
||||||
|
|
||||||
// Re-evaluate CanExecute when worker connection flips.
|
|
||||||
_workerPropertyChangedHandler = (_, e) =>
|
_workerPropertyChangedHandler = (_, e) =>
|
||||||
{
|
{
|
||||||
if (e.PropertyName == nameof(IWorkerClient.IsConnected))
|
if (e.PropertyName == nameof(IWorkerClient.IsConnected))
|
||||||
@@ -558,7 +438,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
};
|
};
|
||||||
_worker.PropertyChanged += _workerPropertyChangedHandler;
|
_worker.PropertyChanged += _workerPropertyChangedHandler;
|
||||||
|
|
||||||
// If the task row's live status changes (e.g. TaskStarted/Finished), mirror it.
|
|
||||||
_workerTaskStartedHandler = (slot, taskId, startedAt) =>
|
_workerTaskStartedHandler = (slot, taskId, startedAt) =>
|
||||||
{
|
{
|
||||||
if (Task?.Id == taskId) AgentState = "running";
|
if (Task?.Id == taskId) AgentState = "running";
|
||||||
@@ -576,7 +455,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
Text = $"── {status.ToUpperInvariant()} · {finishedAt.ToLocalTime():HH:mm:ss} ──",
|
Text = $"── {status.ToUpperInvariant()} · {finishedAt.ToLocalTime():HH:mm:ss} ──",
|
||||||
});
|
});
|
||||||
AgentState = FinishedStatusToStateKey(status);
|
AgentState = FinishedStatusToStateKey(status);
|
||||||
// Re-query to pick up worktree created during the run.
|
|
||||||
_ = RefreshWorktreeAsync(taskId);
|
_ = RefreshWorktreeAsync(taskId);
|
||||||
_ = RefreshChildOutcomeAsync(taskId);
|
_ = RefreshChildOutcomeAsync(taskId);
|
||||||
_ = RefreshOutcomeAsync(taskId);
|
_ = RefreshOutcomeAsync(taskId);
|
||||||
@@ -599,18 +477,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
};
|
};
|
||||||
_worker.TaskUpdatedEvent += _workerTaskUpdatedHandler;
|
_worker.TaskUpdatedEvent += _workerTaskUpdatedHandler;
|
||||||
|
|
||||||
Subtasks.CollectionChanged += (_, _) =>
|
|
||||||
{
|
|
||||||
ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
|
|
||||||
};
|
|
||||||
|
|
||||||
ChildOutcomes.CollectionChanged += (_, _) =>
|
ChildOutcomes.CollectionChanged += (_, _) =>
|
||||||
{
|
{
|
||||||
ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
|
Merge.SyncChildOutcomes(HasChildOutcomes, Subtasks.Count);
|
||||||
NotifySessionSections();
|
NotifySessionSections();
|
||||||
};
|
};
|
||||||
|
|
||||||
PrepLog.CollectionChanged += (_, _) => OnPropertyChanged(nameof(ShowPrepEmptyState));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -622,25 +493,21 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
_worker.WorktreeUpdatedEvent -= _workerWorktreeUpdatedHandler;
|
_worker.WorktreeUpdatedEvent -= _workerWorktreeUpdatedHandler;
|
||||||
_worker.TaskUpdatedEvent -= _workerTaskUpdatedHandler;
|
_worker.TaskUpdatedEvent -= _workerTaskUpdatedHandler;
|
||||||
_worker.TaskMessageEvent -= OnTaskMessage;
|
_worker.TaskMessageEvent -= OnTaskMessage;
|
||||||
_worker.PrepStartedEvent -= OnPrepStarted;
|
AgentSettings.Dispose();
|
||||||
_worker.PrepLineEvent -= OnPrepLine;
|
Prep.Dispose();
|
||||||
_worker.PrepFinishedEvent -= OnPrepFinished;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTaskMessage(string taskId, string line)
|
private void OnTaskMessage(string taskId, string line)
|
||||||
{
|
{
|
||||||
if (taskId != _subscribedTaskId) return;
|
if (taskId != _subscribedTaskId) return;
|
||||||
|
|
||||||
// `[stdout] ...json...` lines are Claude CLI stream-json; parse through the
|
|
||||||
// formatter so the user sees human text, not raw JSON.
|
|
||||||
if (line.StartsWith("[stdout]", StringComparison.OrdinalIgnoreCase))
|
if (line.StartsWith("[stdout]", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var body = line["[stdout]".Length..].TrimStart();
|
var body = line["[stdout]".Length..].TrimStart();
|
||||||
AppendStdoutLine(Log, body);
|
AppendStdoutLine(body);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-stdout tagged lines: flush any buffered text then classify by prefix.
|
|
||||||
FlushClaudeBuffer();
|
FlushClaudeBuffer();
|
||||||
|
|
||||||
var kind = line.StartsWith("[sys]", StringComparison.OrdinalIgnoreCase) ? LogKind.Sys
|
var kind = line.StartsWith("[sys]", StringComparison.OrdinalIgnoreCase) ? LogKind.Sys
|
||||||
@@ -652,52 +519,21 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
Log.Add(new LogLineViewModel { Kind = kind, Text = line });
|
Log.Add(new LogLineViewModel { Kind = kind, Text = line });
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AppendStdoutLine(ObservableCollection<LogLineViewModel> target, string line)
|
private void AppendStdoutLine(string line)
|
||||||
{
|
{
|
||||||
var formatted = _formatter.FormatLine(line);
|
var formatted = _formatter.FormatLine(line);
|
||||||
if (formatted is null) return;
|
if (formatted is null) return;
|
||||||
var buf = ReferenceEquals(target, Log) ? _claudeBuf : _prepClaudeBuf;
|
_claudeBuf.Append(formatted);
|
||||||
AppendClaudeText(formatted, target, buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task PlanDayAsync()
|
|
||||||
{
|
|
||||||
if (_worker is null) return;
|
|
||||||
try { await _worker.RunDailyPrepNowAsync(); }
|
|
||||||
catch { /* worker offline; PrepStarted/PrepLine will reconcile */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ShowPrepEmptyState => !IsPrepRunning && PrepLog.Count == 0;
|
|
||||||
|
|
||||||
partial void OnIsPrepRunningChanged(bool value) => OnPropertyChanged(nameof(ShowPrepEmptyState));
|
|
||||||
|
|
||||||
private void OnPrepStarted()
|
|
||||||
{
|
|
||||||
PrepLog.Clear();
|
|
||||||
IsPrepRunning = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPrepLine(string line) => AppendStdoutLine(PrepLog, line);
|
|
||||||
|
|
||||||
private void OnPrepFinished(bool success) => IsPrepRunning = false;
|
|
||||||
|
|
||||||
private void AppendClaudeText(string chunk) => AppendClaudeText(chunk, Log, _claudeBuf);
|
|
||||||
|
|
||||||
private static void AppendClaudeText(string chunk, ObservableCollection<LogLineViewModel> target, StringBuilder buf)
|
|
||||||
{
|
|
||||||
buf.Append(chunk);
|
|
||||||
// Emit a log entry for every completed line; keep the trailing remainder buffered.
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var text = buf.ToString();
|
var text = _claudeBuf.ToString();
|
||||||
var nl = text.IndexOf('\n');
|
var nl = text.IndexOf('\n');
|
||||||
if (nl < 0) break;
|
if (nl < 0) break;
|
||||||
var piece = text[..nl].TrimEnd('\r');
|
var piece = text[..nl].TrimEnd('\r');
|
||||||
if (!string.IsNullOrWhiteSpace(piece))
|
if (!string.IsNullOrWhiteSpace(piece))
|
||||||
target.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
|
Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
|
||||||
buf.Clear();
|
_claudeBuf.Clear();
|
||||||
buf.Append(text[(nl + 1)..]);
|
_claudeBuf.Append(text[(nl + 1)..]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,9 +546,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
|
Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnTaskSystemPromptChanged(string value) => QueueAgentSave();
|
|
||||||
partial void OnTaskSelectedAgentChanged(AgentInfo? value) { RecomputeAgentBadge(); QueueAgentSave(); }
|
|
||||||
|
|
||||||
partial void OnEditableDescriptionChanged(string value)
|
partial void OnEditableDescriptionChanged(string value)
|
||||||
{
|
{
|
||||||
if (_suppressDescSave || Task is null) return;
|
if (_suppressDescSave || Task is null) return;
|
||||||
@@ -738,76 +571,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void QueueAgentSave()
|
|
||||||
{
|
|
||||||
if (_suppressAgentSave || Task is null) return;
|
|
||||||
_agentSaveCts?.Cancel();
|
|
||||||
_agentSaveCts = new CancellationTokenSource();
|
|
||||||
var ct = _agentSaveCts.Token;
|
|
||||||
_ = SaveAgentSettingsAsync(ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task SaveAgentSettingsAsync(CancellationToken ct)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await System.Threading.Tasks.Task.Delay(300, ct);
|
|
||||||
if (Task is null) return;
|
|
||||||
|
|
||||||
var model = string.IsNullOrWhiteSpace(TaskModelSelection) ? null : TaskModelSelection;
|
|
||||||
var sp = string.IsNullOrWhiteSpace(TaskSystemPrompt) ? null : TaskSystemPrompt;
|
|
||||||
var ap = TaskSelectedAgent is null || string.IsNullOrWhiteSpace(TaskSelectedAgent.Path)
|
|
||||||
? null : TaskSelectedAgent.Path;
|
|
||||||
var turns = TaskMaxTurns is decimal d ? (int?)d : null;
|
|
||||||
|
|
||||||
await _worker.UpdateTaskAgentSettingsAsync(
|
|
||||||
new ClaudeDo.Ui.Services.UpdateTaskAgentSettingsDto(Task.Id, model, sp, ap, turns));
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) { }
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task LoadAgentSettingsAsync(
|
|
||||||
ClaudeDo.Data.Models.TaskEntity entity, CancellationToken ct)
|
|
||||||
{
|
|
||||||
_suppressAgentSave = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
TaskAgentOptions.Clear();
|
|
||||||
TaskAgentOptions.Add(new AgentInfo("(inherited)", "", ""));
|
|
||||||
var agents = await _worker.GetAgentsAsync();
|
|
||||||
foreach (var a in agents) TaskAgentOptions.Add(a);
|
|
||||||
|
|
||||||
TaskModelSelection = string.IsNullOrWhiteSpace(entity.Model) ? null : entity.Model!;
|
|
||||||
TaskMaxTurns = entity.MaxTurns is int tmt ? tmt : (decimal?)null;
|
|
||||||
TaskSystemPrompt = entity.SystemPrompt ?? "";
|
|
||||||
TaskSelectedAgent = string.IsNullOrWhiteSpace(entity.AgentPath)
|
|
||||||
? TaskAgentOptions[0]
|
|
||||||
: (TaskAgentOptions.FirstOrDefault(a => a.Path == entity.AgentPath) ?? TaskAgentOptions[0]);
|
|
||||||
|
|
||||||
var listCfg = await _worker.GetListConfigAsync(entity.ListId);
|
|
||||||
var app = await _worker.GetAppSettingsAsync();
|
|
||||||
_globalModel = app?.DefaultModel ?? ModelRegistry.DefaultAlias;
|
|
||||||
_globalMaxTurns = app?.DefaultMaxTurns ?? 100;
|
|
||||||
_listModel = listCfg?.Model;
|
|
||||||
_listMaxTurns = listCfg?.MaxTurns;
|
|
||||||
_listAgentName = string.IsNullOrWhiteSpace(listCfg?.AgentPath)
|
|
||||||
? null : System.IO.Path.GetFileName(listCfg!.AgentPath!);
|
|
||||||
|
|
||||||
EffectiveSystemPromptHint = string.IsNullOrWhiteSpace(listCfg?.SystemPrompt) ? "" : listCfg!.SystemPrompt!;
|
|
||||||
|
|
||||||
RecomputeModelBadge();
|
|
||||||
RecomputeTurnsBadge();
|
|
||||||
RecomputeAgentBadge();
|
|
||||||
OnPropertyChanged(nameof(EffectiveMaxTurns));
|
|
||||||
OnPropertyChanged(nameof(TurnsText));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_suppressAgentSave = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ShowNotes()
|
public void ShowNotes()
|
||||||
{
|
{
|
||||||
Bind(null);
|
Bind(null);
|
||||||
@@ -821,21 +584,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
Bind(null);
|
Bind(null);
|
||||||
IsNotesMode = false;
|
IsNotesMode = false;
|
||||||
IsPrepMode = true;
|
IsPrepMode = true;
|
||||||
_ = LoadLastPrepLogIfEmptyAsync();
|
_ = Prep.LoadLastPrepLogIfEmptyAsync();
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LoadLastPrepLogIfEmptyAsync()
|
|
||||||
{
|
|
||||||
if (_worker is null || IsPrepRunning || PrepLog.Count > 0) return;
|
|
||||||
string text;
|
|
||||||
try { text = await _worker.GetLastPrepLogAsync(); }
|
|
||||||
catch { return; }
|
|
||||||
if (IsPrepRunning || PrepLog.Count > 0) return;
|
|
||||||
foreach (var line in text.Split('\n'))
|
|
||||||
{
|
|
||||||
var trimmed = line.TrimEnd('\r');
|
|
||||||
if (trimmed.Length > 0) AppendStdoutLine(PrepLog, trimmed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Bind(TaskRowViewModel? row)
|
public void Bind(TaskRowViewModel? row)
|
||||||
@@ -853,11 +602,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
Subtasks.Clear();
|
Subtasks.Clear();
|
||||||
ChildOutcomes.Clear();
|
ChildOutcomes.Clear();
|
||||||
OnPropertyChanged(nameof(HasChildOutcomes));
|
OnPropertyChanged(nameof(HasChildOutcomes));
|
||||||
MergeTargetBranches.Clear();
|
|
||||||
SelectedMergeTarget = null;
|
|
||||||
SessionOutcome = null;
|
SessionOutcome = null;
|
||||||
Roadblocks = null;
|
Roadblocks = null;
|
||||||
_claudeBuf.Clear();
|
_claudeBuf.Clear();
|
||||||
|
Merge.Clear();
|
||||||
|
|
||||||
if (row == null)
|
if (row == null)
|
||||||
{
|
{
|
||||||
@@ -874,19 +622,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
DiffDeletions = 0;
|
DiffDeletions = 0;
|
||||||
AgentState = "idle";
|
AgentState = "idle";
|
||||||
LatestRunSessionId = null;
|
LatestRunSessionId = null;
|
||||||
_suppressAgentSave = true;
|
AgentSettings.Clear();
|
||||||
try
|
|
||||||
{
|
|
||||||
TaskModelSelection = null;
|
|
||||||
TaskMaxTurns = null;
|
|
||||||
TaskSystemPrompt = "";
|
|
||||||
TaskSelectedAgent = null;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_suppressAgentSave = false;
|
|
||||||
}
|
|
||||||
EffectiveSystemPromptHint = "";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -900,7 +636,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||||
var subtaskRepo = new SubtaskRepository(ctx);
|
var subtaskRepo = new SubtaskRepository(ctx);
|
||||||
|
|
||||||
// Own query with Include so WorktreePath/BranchLine are populated.
|
|
||||||
var entity = await ctx.Tasks
|
var entity = await ctx.Tasks
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(t => t.Worktree)
|
.Include(t => t.Worktree)
|
||||||
@@ -919,12 +654,18 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
WorktreeBaseCommit = entity.Worktree?.BaseCommit;
|
WorktreeBaseCommit = entity.Worktree?.BaseCommit;
|
||||||
WorktreeHeadCommit = entity.Worktree?.HeadCommit;
|
WorktreeHeadCommit = entity.Worktree?.HeadCommit;
|
||||||
WorktreeStateLabel = entity.Worktree?.State.ToString();
|
WorktreeStateLabel = entity.Worktree?.State.ToString();
|
||||||
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null;
|
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null;
|
||||||
var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat);
|
var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat);
|
||||||
DiffAdditions = add;
|
DiffAdditions = add;
|
||||||
DiffDeletions = del;
|
DiffDeletions = del;
|
||||||
AgentState = StatusToStateKey(entity.Status);
|
AgentState = StatusToStateKey(entity.Status);
|
||||||
await LoadAgentSettingsAsync(entity, ct);
|
|
||||||
|
Merge.SyncTaskContext(row.Id, row.Title, row.IsPlanningParent);
|
||||||
|
Merge.SyncWorktree(WorktreePath, WorktreeBaseCommit, WorktreeHeadCommit,
|
||||||
|
WorktreeStateLabel, _listWorkingDir);
|
||||||
|
|
||||||
|
AgentSettings.TaskId = row.Id;
|
||||||
|
await AgentSettings.LoadAsync(entity, ct);
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var runRepo = new TaskRunRepository(ctx);
|
var runRepo = new TaskRunRepository(ctx);
|
||||||
@@ -933,10 +674,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
LatestRunSessionId = latestRun?.SessionId;
|
LatestRunSessionId = latestRun?.SessionId;
|
||||||
ApplyOutcome(entity.Result, latestRun?.ErrorMarkdown);
|
ApplyOutcome(entity.Result, latestRun?.ErrorMarkdown);
|
||||||
|
|
||||||
// Subscribe only after DB load confirms the task exists
|
|
||||||
_subscribedTaskId = row.Id;
|
_subscribedTaskId = row.Id;
|
||||||
|
|
||||||
// Replay the latest run's persisted log so output is visible across app restarts.
|
|
||||||
await ReplayLogFileAsync(entity.LogPath, ct);
|
await ReplayLogFileAsync(entity.LogPath, ct);
|
||||||
|
|
||||||
var subs = await subtaskRepo.GetByTaskIdAsync(row.Id);
|
var subs = await subtaskRepo.GetByTaskIdAsync(row.Id);
|
||||||
@@ -946,31 +685,25 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
if (entity.PlanningPhase != ClaudeDo.Data.Models.PlanningPhase.None)
|
if (entity.PlanningPhase != ClaudeDo.Data.Models.PlanningPhase.None)
|
||||||
await LoadPlanningChildrenAsync(row.Id, ct);
|
await LoadPlanningChildrenAsync(row.Id, ct);
|
||||||
// Surface every parent's children — planning or improvement — in the
|
|
||||||
// Session tab with their live status + roadblock count. This is what
|
|
||||||
// makes the Session tab appear for planning parents and lets a child's
|
|
||||||
// roadblock register on the parent.
|
|
||||||
await LoadChildOutcomesAsync(row.Id, ct);
|
await LoadChildOutcomesAsync(row.Id, ct);
|
||||||
|
|
||||||
if (entity.Worktree != null
|
if (entity.Worktree != null
|
||||||
&& entity.PlanningPhase == ClaudeDo.Data.Models.PlanningPhase.None
|
&& entity.PlanningPhase == ClaudeDo.Data.Models.PlanningPhase.None
|
||||||
&& MergeTargetBranches.Count == 0)
|
&& Merge.MergeTargetBranches.Count == 0)
|
||||||
{
|
{
|
||||||
var targets = await _worker.GetMergeTargetsAsync(row.Id);
|
var targets = await _worker.GetMergeTargetsAsync(row.Id);
|
||||||
if (targets != null)
|
if (targets != null)
|
||||||
{
|
{
|
||||||
MergeTargetBranches.Clear();
|
Merge.MergeTargetBranches.Clear();
|
||||||
foreach (var b in targets.LocalBranches) MergeTargetBranches.Add(b);
|
foreach (var b in targets.LocalBranches) Merge.MergeTargetBranches.Add(b);
|
||||||
SelectedMergeTarget = targets.DefaultBranch; // triggers OnSelectedMergeTargetChanged → preview
|
Merge.SelectedMergeTarget = targets.DefaultBranch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await RefreshMergePreviewAsync();
|
await Merge.RefreshMergePreviewAsync();
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Improvement parents (non-planning) surface their children's outcomes + roadblocks
|
|
||||||
// on the review card, and reuse the planning merge controls to fold the tree in.
|
|
||||||
private async System.Threading.Tasks.Task LoadChildOutcomesAsync(string parentTaskId, CancellationToken ct)
|
private async System.Threading.Tasks.Task LoadChildOutcomesAsync(string parentTaskId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -997,7 +730,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
});
|
});
|
||||||
OnPropertyChanged(nameof(HasChildOutcomes));
|
OnPropertyChanged(nameof(HasChildOutcomes));
|
||||||
|
|
||||||
if (MergeTargetBranches.Count == 0)
|
if (Merge.MergeTargetBranches.Count == 0)
|
||||||
{
|
{
|
||||||
var childWithWorktree = children.FirstOrDefault(c => c.Worktree != null);
|
var childWithWorktree = children.FirstOrDefault(c => c.Worktree != null);
|
||||||
if (childWithWorktree != null)
|
if (childWithWorktree != null)
|
||||||
@@ -1005,14 +738,13 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
var targets = await _worker.GetMergeTargetsAsync(childWithWorktree.Id);
|
var targets = await _worker.GetMergeTargetsAsync(childWithWorktree.Id);
|
||||||
if (targets != null)
|
if (targets != null)
|
||||||
{
|
{
|
||||||
MergeTargetBranches.Clear();
|
Merge.MergeTargetBranches.Clear();
|
||||||
foreach (var b in targets.LocalBranches)
|
foreach (var b in targets.LocalBranches)
|
||||||
MergeTargetBranches.Add(b);
|
Merge.MergeTargetBranches.Add(b);
|
||||||
SelectedMergeTarget = targets.DefaultBranch;
|
Merge.SelectedMergeTarget = targets.DefaultBranch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException) { }
|
||||||
catch { /* best-effort */ }
|
catch { /* best-effort */ }
|
||||||
@@ -1047,9 +779,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
if (_subscribedTaskId is null) return;
|
if (_subscribedTaskId is null) return;
|
||||||
// Worker writes raw Claude CLI stdout to disk (no prefix) but broadcasts
|
|
||||||
// it with a "[stdout] " prefix. Match the live-stream format so the same
|
|
||||||
// stream-json parser handles both.
|
|
||||||
var line = all[i];
|
var line = all[i];
|
||||||
var normalized = line.StartsWith("[", StringComparison.Ordinal) ? line : "[stdout] " + line;
|
var normalized = line.StartsWith("[", StringComparison.Ordinal) ? line : "[stdout] " + line;
|
||||||
OnTaskMessage(_subscribedTaskId, normalized);
|
OnTaskMessage(_subscribedTaskId, normalized);
|
||||||
@@ -1093,7 +822,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MergeTargetBranches.Count == 0)
|
if (Merge.MergeTargetBranches.Count == 0)
|
||||||
{
|
{
|
||||||
var childWithWorktree = children.FirstOrDefault(c => c.Worktree != null);
|
var childWithWorktree = children.FirstOrDefault(c => c.Worktree != null);
|
||||||
if (childWithWorktree != null)
|
if (childWithWorktree != null)
|
||||||
@@ -1101,14 +830,13 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
var targets = await _worker.GetMergeTargetsAsync(childWithWorktree.Id);
|
var targets = await _worker.GetMergeTargetsAsync(childWithWorktree.Id);
|
||||||
if (targets != null)
|
if (targets != null)
|
||||||
{
|
{
|
||||||
MergeTargetBranches.Clear();
|
Merge.MergeTargetBranches.Clear();
|
||||||
foreach (var b in targets.LocalBranches)
|
foreach (var b in targets.LocalBranches)
|
||||||
MergeTargetBranches.Add(b);
|
Merge.MergeTargetBranches.Add(b);
|
||||||
SelectedMergeTarget = targets.DefaultBranch;
|
Merge.SelectedMergeTarget = targets.DefaultBranch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException) { }
|
||||||
catch { /* best-effort */ }
|
catch { /* best-effort */ }
|
||||||
@@ -1132,13 +860,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
existing.Status = child.Status;
|
existing.Status = child.Status;
|
||||||
existing.WorktreeState = child.Worktree?.State ?? ClaudeDo.Data.Models.WorktreeState.Active;
|
existing.WorktreeState = child.Worktree?.State ?? ClaudeDo.Data.Models.WorktreeState.Active;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
catch { /* best-effort */ }
|
catch { /* best-effort */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Live-update a single improvement child's outcome row from a task event. No-op if the
|
|
||||||
// updated task isn't one of this parent's children.
|
|
||||||
private async System.Threading.Tasks.Task RefreshChildOutcomeAsync(string childTaskId)
|
private async System.Threading.Tasks.Task RefreshChildOutcomeAsync(string childTaskId)
|
||||||
{
|
{
|
||||||
var row = ChildOutcomes.FirstOrDefault(c => c.Id == childTaskId);
|
var row = ChildOutcomes.FirstOrDefault(c => c.Id == childTaskId);
|
||||||
@@ -1154,23 +879,12 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
row.Status = child.Status;
|
row.Status = child.Status;
|
||||||
row.RoadblockCount = child.RoadblockCount;
|
row.RoadblockCount = child.RoadblockCount;
|
||||||
row.WorktreeState = child.Worktree?.State ?? ClaudeDo.Data.Models.WorktreeState.Active;
|
row.WorktreeState = child.Worktree?.State ?? ClaudeDo.Data.Models.WorktreeState.Active;
|
||||||
ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
|
Merge.ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
|
||||||
NotifyAttention();
|
NotifyAttention();
|
||||||
}
|
}
|
||||||
catch { /* best-effort */ }
|
catch { /* best-effort */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanReviewDiff))]
|
|
||||||
private async System.Threading.Tasks.Task ReviewCombinedDiffAsync()
|
|
||||||
{
|
|
||||||
if (Task is null || ShowPlanningDiffModal is null) return;
|
|
||||||
var vm = new ViewModels.Planning.PlanningDiffViewModel(_worker, Task.Id, SelectedMergeTarget ?? "main");
|
|
||||||
await vm.InitializeAsync();
|
|
||||||
await ShowPlanningDiffModal(vm);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CanReviewDiff() => (Task?.IsPlanningParent == true && Subtasks.Any()) || HasChildOutcomes;
|
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task RefreshWorktreeAsync(string taskId)
|
private async System.Threading.Tasks.Task RefreshWorktreeAsync(string taskId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -1195,116 +909,29 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat);
|
var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat);
|
||||||
DiffAdditions = add;
|
DiffAdditions = add;
|
||||||
DiffDeletions = del;
|
DiffDeletions = del;
|
||||||
|
|
||||||
|
Merge.SyncWorktree(WorktreePath, WorktreeBaseCommit, WorktreeHeadCommit,
|
||||||
|
WorktreeStateLabel, _listWorkingDir);
|
||||||
}
|
}
|
||||||
catch { /* best-effort refresh */ }
|
catch { /* best-effort refresh */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task RefreshMergePreviewAsync()
|
|
||||||
{
|
|
||||||
if (Task is null || WorktreePath is null)
|
|
||||||
{
|
|
||||||
MergePreviewText = ""; MergeIsClean = false; MergeIsConflict = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Only probe Active worktrees; terminal states show their label instead.
|
|
||||||
if (WorktreeStateLabel is { } label && label != "Active")
|
|
||||||
{
|
|
||||||
MergePreviewText = label; MergeIsClean = false; MergeIsConflict = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var capturedTaskId = Task.Id;
|
|
||||||
var capturedTarget = SelectedMergeTarget;
|
|
||||||
var dto = await _worker.PreviewMergeAsync(capturedTaskId, capturedTarget ?? "");
|
|
||||||
// Discard a probe that resolved after the user switched task or target.
|
|
||||||
if (Task?.Id != capturedTaskId || SelectedMergeTarget != capturedTarget) return;
|
|
||||||
var (text, clean, conflict) = MergePreviewPresenter.Describe(dto);
|
|
||||||
MergePreviewText = text; MergeIsClean = clean; MergeIsConflict = conflict;
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanOpenDiff))]
|
|
||||||
private async System.Threading.Tasks.Task OpenDiffAsync()
|
|
||||||
{
|
|
||||||
if (ShowDiffModal == null) return;
|
|
||||||
var git = _services.GetRequiredService<ClaudeDo.Data.Git.GitService>();
|
|
||||||
|
|
||||||
// Active worktree on disk → diff the worktree live (and allow merging from it).
|
|
||||||
var hasLiveWorktree =
|
|
||||||
WorktreePath != null
|
|
||||||
&& WorktreeStateLabel == "Active"
|
|
||||||
&& System.IO.Directory.Exists(WorktreePath);
|
|
||||||
|
|
||||||
DiffModalViewModel diffVm;
|
|
||||||
if (hasLiveWorktree)
|
|
||||||
{
|
|
||||||
diffVm = new DiffModalViewModel(git)
|
|
||||||
{
|
|
||||||
WorktreePath = WorktreePath!,
|
|
||||||
BaseRef = WorktreeBaseCommit,
|
|
||||||
TaskId = Task?.Id,
|
|
||||||
TaskTitle = Task?.Title ?? "",
|
|
||||||
ShowMergeModal = ShowMergeModal,
|
|
||||||
ResolveMergeVm = () => _services.GetRequiredService<MergeModalViewModel>(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (CanDiffMergedRange)
|
|
||||||
{
|
|
||||||
// Worktree is gone (merged/discarded) but the commits survive on the
|
|
||||||
// target branch — diff the captured base..head range in the repo. No
|
|
||||||
// merge action: the work is already integrated.
|
|
||||||
diffVm = new DiffModalViewModel(git)
|
|
||||||
{
|
|
||||||
WorktreePath = _listWorkingDir!,
|
|
||||||
BaseRef = WorktreeBaseCommit,
|
|
||||||
HeadCommit = WorktreeHeadCommit,
|
|
||||||
FromCommitRange = true,
|
|
||||||
TaskId = Task?.Id,
|
|
||||||
TaskTitle = Task?.Title ?? "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else return;
|
|
||||||
|
|
||||||
await diffVm.LoadAsync();
|
|
||||||
await ShowDiffModal(diffVm);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CanDiffMergedRange =>
|
|
||||||
WorktreeBaseCommit != null && WorktreeHeadCommit != null && _listWorkingDir != null;
|
|
||||||
|
|
||||||
private bool CanOpenDiff() => WorktreePath != null || CanDiffMergedRange;
|
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanOpenWorktree))]
|
|
||||||
private void OpenWorktree()
|
|
||||||
{
|
|
||||||
if (WorktreePath == null) return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = WorktreePath,
|
|
||||||
UseShellExecute = true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch { /* explorer open is best-effort */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CanOpenWorktree() => WorktreePath != null;
|
|
||||||
|
|
||||||
partial void OnSelectedMergeTargetChanged(string? value)
|
|
||||||
{
|
|
||||||
_ = RefreshMergePreviewAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnWorktreePathChanged(string? value)
|
partial void OnWorktreePathChanged(string? value)
|
||||||
{
|
{
|
||||||
OpenDiffCommand.NotifyCanExecuteChanged();
|
Merge.SyncWorktree(WorktreePath, WorktreeBaseCommit, WorktreeHeadCommit,
|
||||||
OpenWorktreeCommand.NotifyCanExecuteChanged();
|
WorktreeStateLabel, _listWorkingDir);
|
||||||
NotifySessionSections();
|
NotifySessionSections();
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnWorktreeHeadCommitChanged(string? value) =>
|
partial void OnWorktreeHeadCommitChanged(string? value) =>
|
||||||
OpenDiffCommand.NotifyCanExecuteChanged();
|
Merge.SyncWorktree(WorktreePath, WorktreeBaseCommit, WorktreeHeadCommit,
|
||||||
|
WorktreeStateLabel, _listWorkingDir);
|
||||||
|
|
||||||
partial void OnTaskChanged(TaskRowViewModel? value) => NotifySessionSections();
|
partial void OnTaskChanged(TaskRowViewModel? value)
|
||||||
|
{
|
||||||
|
Merge.SyncTaskContext(Task?.Id, Task?.Title, Task?.IsPlanningParent == true);
|
||||||
|
NotifySessionSections();
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void CloseDetails() => CloseDetail?.Invoke();
|
private void CloseDetails() => CloseDetail?.Invoke();
|
||||||
@@ -1370,7 +997,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
var repo = new TaskRepository(ctx);
|
var repo = new TaskRepository(ctx);
|
||||||
await repo.DeleteAsync(row.Id);
|
await repo.DeleteAsync(row.Id);
|
||||||
}
|
}
|
||||||
catch (DbUpdateException ex) when (
|
catch (Microsoft.EntityFrameworkCore.DbUpdateException ex) when (
|
||||||
ex.Message.Contains("FOREIGN KEY", StringComparison.OrdinalIgnoreCase)
|
ex.Message.Contains("FOREIGN KEY", StringComparison.OrdinalIgnoreCase)
|
||||||
|| ex.InnerException?.Message.Contains("FOREIGN KEY", StringComparison.OrdinalIgnoreCase) == true)
|
|| ex.InnerException?.Message.Contains("FOREIGN KEY", StringComparison.OrdinalIgnoreCase) == true)
|
||||||
{
|
{
|
||||||
@@ -1393,7 +1020,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
await using var ctx = _dbFactory.CreateDbContext();
|
await using var ctx = _dbFactory.CreateDbContext();
|
||||||
var repo = new SubtaskRepository(ctx);
|
var repo = new SubtaskRepository(ctx);
|
||||||
|
|
||||||
// Emptying the text removes the step.
|
|
||||||
if (string.IsNullOrEmpty(title))
|
if (string.IsNullOrEmpty(title))
|
||||||
{
|
{
|
||||||
await repo.DeleteAsync(row.Id);
|
await repo.DeleteAsync(row.Id);
|
||||||
@@ -1512,13 +1138,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
private bool CanResetAndRetry() =>
|
private bool CanResetAndRetry() =>
|
||||||
Task != null && _worker.IsConnected && ShowResetAndRetry;
|
Task != null && _worker.IsConnected && ShowResetAndRetry;
|
||||||
|
|
||||||
[RelayCommand] private void ResetTaskModel() => TaskModelSelection = null;
|
|
||||||
[RelayCommand] private void ResetTaskTurns() => TaskMaxTurns = null;
|
|
||||||
[RelayCommand] private void ResetTaskAgent() => TaskSelectedAgent = TaskAgentOptions.Count > 0 ? TaskAgentOptions[0] : null;
|
|
||||||
|
|
||||||
// ── Review actions ──────────────────────────────────────────────────────────
|
|
||||||
[ObservableProperty] private string _reviewFeedback = "";
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async System.Threading.Tasks.Task ApproveReviewAsync()
|
private async System.Threading.Tasks.Task ApproveReviewAsync()
|
||||||
{
|
{
|
||||||
@@ -1526,26 +1145,25 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var hasChildren = Subtasks.Count > 0 || ChildOutcomes.Count > 0;
|
var hasChildren = Subtasks.Count > 0 || ChildOutcomes.Count > 0;
|
||||||
var result = await _worker.ApproveReviewAsync(Task.Id, SelectedMergeTarget ?? "");
|
var result = await _worker.ApproveReviewAsync(Task.Id, Merge.SelectedMergeTarget ?? "");
|
||||||
if (!hasChildren && result?.Status == "conflict")
|
if (!hasChildren && result?.Status == "conflict")
|
||||||
{
|
{
|
||||||
if (RequestConflictResolution is not null)
|
if (Merge.RequestConflictResolution is not null)
|
||||||
{
|
{
|
||||||
await RequestConflictResolution(Task.Id, SelectedMergeTarget ?? "");
|
await Merge.RequestConflictResolution(Task.Id, Merge.SelectedMergeTarget ?? "");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var (text, _, _) = MergePreviewPresenter.Describe(
|
var (text, _, _) = MergePreviewPresenter.Describe(
|
||||||
new MergePreviewDto("conflict", result.ConflictFiles, 0));
|
new MergePreviewDto("conflict", result.ConflictFiles, 0));
|
||||||
MergePreviewText = text; MergeIsClean = false; MergeIsConflict = true;
|
Merge.MergePreviewText = text;
|
||||||
|
Merge.MergeIsClean = false;
|
||||||
|
Merge.MergeIsConflict = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// hasChildren: conflicts arrive via PlanningMergeConflictEvent → conflict dialog
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// A real failure (e.g. a child still needs attention, so the unit can't
|
|
||||||
// be approved yet) must not vanish — tell the user why nothing happened.
|
|
||||||
if (ShowErrorAsync != null)
|
if (ShowErrorAsync != null)
|
||||||
await ShowErrorAsync(ex.Message);
|
await ShowErrorAsync(ex.Message);
|
||||||
}
|
}
|
||||||
@@ -1582,7 +1200,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
catch { /* stale review action; broadcast reconciles */ }
|
catch { /* stale review action; broadcast reconciles */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Diff meter parser ───────────────────────────────────────────────────────
|
|
||||||
internal static (int Additions, int Deletions) ParseDiffStat(string? stat)
|
internal static (int Additions, int Deletions) ParseDiffStat(string? stat)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(stat)) return (0, 0);
|
if (string.IsNullOrEmpty(stat)) return (0, 0);
|
||||||
|
|||||||
200
src/ClaudeDo.Ui/ViewModels/Islands/MergeSectionViewModel.cs
Normal file
200
src/ClaudeDo.Ui/ViewModels/Islands/MergeSectionViewModel.cs
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
|
public sealed partial class MergeSectionViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IWorkerClient _worker;
|
||||||
|
private readonly IServiceProvider _services;
|
||||||
|
|
||||||
|
// Context mirrored from parent, updated via Sync* methods
|
||||||
|
internal string? TaskId { get; private set; }
|
||||||
|
internal string? TaskTitle { get; private set; }
|
||||||
|
private string? _worktreePath;
|
||||||
|
private string? _worktreeBaseCommit;
|
||||||
|
private string? _worktreeHeadCommit;
|
||||||
|
private string? _worktreeStateLabel;
|
||||||
|
private string? _listWorkingDir;
|
||||||
|
private bool _isPlanningParent;
|
||||||
|
private int _subtaskCount;
|
||||||
|
private bool _hasChildOutcomes;
|
||||||
|
|
||||||
|
[ObservableProperty] private ObservableCollection<string> _mergeTargetBranches = new();
|
||||||
|
[ObservableProperty] private string? _selectedMergeTarget;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowMergePreviewMuted))]
|
||||||
|
private string _mergePreviewText = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowMergePreviewMuted))]
|
||||||
|
private bool _mergeIsClean;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowMergePreviewMuted))]
|
||||||
|
private bool _mergeIsConflict;
|
||||||
|
|
||||||
|
public bool ShowMergePreviewMuted =>
|
||||||
|
!MergeIsClean && !MergeIsConflict && !string.IsNullOrEmpty(MergePreviewText);
|
||||||
|
|
||||||
|
public bool ShowMergeSection =>
|
||||||
|
_worktreePath != null || _isPlanningParent || _hasChildOutcomes;
|
||||||
|
|
||||||
|
public Func<DiffModalViewModel, System.Threading.Tasks.Task>? ShowDiffModal { get; set; }
|
||||||
|
public Func<MergeModalViewModel, System.Threading.Tasks.Task>? ShowMergeModal { get; set; }
|
||||||
|
public Func<ViewModels.Planning.PlanningDiffViewModel, System.Threading.Tasks.Task>? ShowPlanningDiffModal { get; set; }
|
||||||
|
public Func<string, string, System.Threading.Tasks.Task>? RequestConflictResolution { get; set; }
|
||||||
|
|
||||||
|
public MergeSectionViewModel(IWorkerClient worker, IServiceProvider services)
|
||||||
|
{
|
||||||
|
_worker = worker;
|
||||||
|
_services = services;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedMergeTargetChanged(string? value) => _ = RefreshMergePreviewAsync();
|
||||||
|
|
||||||
|
internal void SyncWorktree(
|
||||||
|
string? worktreePath,
|
||||||
|
string? worktreeBase,
|
||||||
|
string? worktreeHead,
|
||||||
|
string? worktreeState,
|
||||||
|
string? listWorkDir)
|
||||||
|
{
|
||||||
|
_worktreePath = worktreePath;
|
||||||
|
_worktreeBaseCommit = worktreeBase;
|
||||||
|
_worktreeHeadCommit = worktreeHead;
|
||||||
|
_worktreeStateLabel = worktreeState;
|
||||||
|
_listWorkingDir = listWorkDir;
|
||||||
|
OnPropertyChanged(nameof(ShowMergeSection));
|
||||||
|
OpenDiffCommand.NotifyCanExecuteChanged();
|
||||||
|
OpenWorktreeCommand.NotifyCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SyncTaskContext(string? taskId, string? taskTitle, bool isPlanningParent)
|
||||||
|
{
|
||||||
|
TaskId = taskId;
|
||||||
|
TaskTitle = taskTitle;
|
||||||
|
_isPlanningParent = isPlanningParent;
|
||||||
|
OnPropertyChanged(nameof(ShowMergeSection));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SyncChildOutcomes(bool hasChildOutcomes, int subtaskCount)
|
||||||
|
{
|
||||||
|
_hasChildOutcomes = hasChildOutcomes;
|
||||||
|
_subtaskCount = subtaskCount;
|
||||||
|
OnPropertyChanged(nameof(ShowMergeSection));
|
||||||
|
ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async System.Threading.Tasks.Task RefreshMergePreviewAsync()
|
||||||
|
{
|
||||||
|
if (TaskId is null || _worktreePath is null)
|
||||||
|
{
|
||||||
|
MergePreviewText = ""; MergeIsClean = false; MergeIsConflict = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_worktreeStateLabel is { } label && label != "Active")
|
||||||
|
{
|
||||||
|
MergePreviewText = label; MergeIsClean = false; MergeIsConflict = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var capturedTaskId = TaskId;
|
||||||
|
var capturedTarget = SelectedMergeTarget;
|
||||||
|
var dto = await _worker.PreviewMergeAsync(capturedTaskId, capturedTarget ?? "");
|
||||||
|
if (TaskId != capturedTaskId || SelectedMergeTarget != capturedTarget) return;
|
||||||
|
var (text, clean, conflict) = MergePreviewPresenter.Describe(dto);
|
||||||
|
MergePreviewText = text; MergeIsClean = clean; MergeIsConflict = conflict;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Clear()
|
||||||
|
{
|
||||||
|
MergeTargetBranches.Clear();
|
||||||
|
SelectedMergeTarget = null;
|
||||||
|
MergePreviewText = "";
|
||||||
|
MergeIsClean = false;
|
||||||
|
MergeIsConflict = false;
|
||||||
|
SyncWorktree(null, null, null, null, null);
|
||||||
|
SyncTaskContext(null, null, false);
|
||||||
|
SyncChildOutcomes(false, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanReviewDiff))]
|
||||||
|
private async System.Threading.Tasks.Task ReviewCombinedDiffAsync()
|
||||||
|
{
|
||||||
|
if (TaskId is null || ShowPlanningDiffModal is null) return;
|
||||||
|
var vm = new ViewModels.Planning.PlanningDiffViewModel(_worker, TaskId, SelectedMergeTarget ?? "main");
|
||||||
|
await vm.InitializeAsync();
|
||||||
|
await ShowPlanningDiffModal(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanReviewDiff() => (_isPlanningParent && _subtaskCount > 0) || _hasChildOutcomes;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanOpenDiff))]
|
||||||
|
private async System.Threading.Tasks.Task OpenDiffAsync()
|
||||||
|
{
|
||||||
|
if (ShowDiffModal is null) return;
|
||||||
|
var git = _services.GetRequiredService<ClaudeDo.Data.Git.GitService>();
|
||||||
|
|
||||||
|
var hasLiveWorktree =
|
||||||
|
_worktreePath != null
|
||||||
|
&& _worktreeStateLabel == "Active"
|
||||||
|
&& System.IO.Directory.Exists(_worktreePath);
|
||||||
|
|
||||||
|
DiffModalViewModel diffVm;
|
||||||
|
if (hasLiveWorktree)
|
||||||
|
{
|
||||||
|
diffVm = new DiffModalViewModel(git)
|
||||||
|
{
|
||||||
|
WorktreePath = _worktreePath!,
|
||||||
|
BaseRef = _worktreeBaseCommit,
|
||||||
|
TaskId = TaskId,
|
||||||
|
TaskTitle = TaskTitle ?? "",
|
||||||
|
ShowMergeModal = ShowMergeModal,
|
||||||
|
ResolveMergeVm = () => _services.GetRequiredService<MergeModalViewModel>(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (CanDiffMergedRange)
|
||||||
|
{
|
||||||
|
diffVm = new DiffModalViewModel(git)
|
||||||
|
{
|
||||||
|
WorktreePath = _listWorkingDir!,
|
||||||
|
BaseRef = _worktreeBaseCommit,
|
||||||
|
HeadCommit = _worktreeHeadCommit,
|
||||||
|
FromCommitRange = true,
|
||||||
|
TaskId = TaskId,
|
||||||
|
TaskTitle = TaskTitle ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else return;
|
||||||
|
|
||||||
|
await diffVm.LoadAsync();
|
||||||
|
await ShowDiffModal(diffVm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanDiffMergedRange =>
|
||||||
|
_worktreeBaseCommit != null && _worktreeHeadCommit != null && _listWorkingDir != null;
|
||||||
|
|
||||||
|
private bool CanOpenDiff() => _worktreePath != null || CanDiffMergedRange;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanOpenWorktree))]
|
||||||
|
private void OpenWorktree()
|
||||||
|
{
|
||||||
|
if (_worktreePath is null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = _worktreePath,
|
||||||
|
UseShellExecute = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanOpenWorktree() => _worktreePath != null;
|
||||||
|
}
|
||||||
102
src/ClaudeDo.Ui/ViewModels/Islands/PrepPanelViewModel.cs
Normal file
102
src/ClaudeDo.Ui/ViewModels/Islands/PrepPanelViewModel.cs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Text;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using ClaudeDo.Ui.Helpers;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
|
public sealed partial class PrepPanelViewModel : ViewModelBase, IDisposable
|
||||||
|
{
|
||||||
|
private readonly IWorkerClient _worker;
|
||||||
|
private readonly StreamLineFormatter _formatter = new();
|
||||||
|
private readonly StringBuilder _prepClaudeBuf = new();
|
||||||
|
|
||||||
|
private readonly Action _onPrepStartedHandler;
|
||||||
|
private readonly Action<string> _onPrepLineHandler;
|
||||||
|
private readonly Action<bool> _onPrepFinishedHandler;
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isPrepRunning;
|
||||||
|
|
||||||
|
public ObservableCollection<LogLineViewModel> PrepLog { get; } = new();
|
||||||
|
|
||||||
|
public bool ShowPrepEmptyState => !IsPrepRunning && PrepLog.Count == 0;
|
||||||
|
|
||||||
|
partial void OnIsPrepRunningChanged(bool value) => OnPropertyChanged(nameof(ShowPrepEmptyState));
|
||||||
|
|
||||||
|
public PrepPanelViewModel(IWorkerClient worker)
|
||||||
|
{
|
||||||
|
_worker = worker;
|
||||||
|
_onPrepStartedHandler = OnPrepStarted;
|
||||||
|
_onPrepLineHandler = OnPrepLine;
|
||||||
|
_onPrepFinishedHandler = OnPrepFinished;
|
||||||
|
|
||||||
|
_worker.PrepStartedEvent += _onPrepStartedHandler;
|
||||||
|
_worker.PrepLineEvent += _onPrepLineHandler;
|
||||||
|
_worker.PrepFinishedEvent += _onPrepFinishedHandler;
|
||||||
|
|
||||||
|
PrepLog.CollectionChanged += (_, _) => OnPropertyChanged(nameof(ShowPrepEmptyState));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_worker.PrepStartedEvent -= _onPrepStartedHandler;
|
||||||
|
_worker.PrepLineEvent -= _onPrepLineHandler;
|
||||||
|
_worker.PrepFinishedEvent -= _onPrepFinishedHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async System.Threading.Tasks.Task PlanDayAsync()
|
||||||
|
{
|
||||||
|
try { await _worker.RunDailyPrepNowAsync(); }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async System.Threading.Tasks.Task LoadLastPrepLogIfEmptyAsync()
|
||||||
|
{
|
||||||
|
if (IsPrepRunning || PrepLog.Count > 0) return;
|
||||||
|
string text;
|
||||||
|
try { text = await _worker.GetLastPrepLogAsync(); }
|
||||||
|
catch { return; }
|
||||||
|
if (IsPrepRunning || PrepLog.Count > 0) return;
|
||||||
|
foreach (var line in text.Split('\n'))
|
||||||
|
{
|
||||||
|
var trimmed = line.TrimEnd('\r');
|
||||||
|
if (trimmed.Length > 0) AppendStdoutLine(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPrepStarted()
|
||||||
|
{
|
||||||
|
PrepLog.Clear();
|
||||||
|
IsPrepRunning = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPrepLine(string line) => AppendStdoutLine(line);
|
||||||
|
|
||||||
|
private void OnPrepFinished(bool success) => IsPrepRunning = false;
|
||||||
|
|
||||||
|
private void AppendStdoutLine(string line)
|
||||||
|
{
|
||||||
|
var formatted = _formatter.FormatLine(line);
|
||||||
|
if (formatted is null) return;
|
||||||
|
AppendClaudeText(formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendClaudeText(string chunk)
|
||||||
|
{
|
||||||
|
_prepClaudeBuf.Append(chunk);
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var text = _prepClaudeBuf.ToString();
|
||||||
|
var nl = text.IndexOf('\n');
|
||||||
|
if (nl < 0) break;
|
||||||
|
var piece = text[..nl].TrimEnd('\r');
|
||||||
|
if (!string.IsNullOrWhiteSpace(piece))
|
||||||
|
PrepLog.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
|
||||||
|
_prepClaudeBuf.Clear();
|
||||||
|
_prepClaudeBuf.Append(text[(nl + 1)..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -138,8 +138,8 @@
|
|||||||
|
|
||||||
<!-- Action buttons -->
|
<!-- Action buttons -->
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,4,0,0">
|
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,4,0,0">
|
||||||
<Button Classes="btn" Content="{loc:Tr agent.openDiff}" Command="{Binding OpenDiffCommand}"/>
|
<Button Classes="btn" Content="{loc:Tr agent.openDiff}" Command="{Binding Merge.OpenDiffCommand}"/>
|
||||||
<Button Classes="btn" Command="{Binding OpenWorktreeCommand}"
|
<Button Classes="btn" Command="{Binding Merge.OpenWorktreeCommand}"
|
||||||
ToolTip.Tip="{loc:Tr agent.openWorktreeTip}">
|
ToolTip.Tip="{loc:Tr agent.openWorktreeTip}">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||||
<PathIcon Data="{StaticResource Icon.ArrowOut}"
|
<PathIcon Data="{StaticResource Icon.ArrowOut}"
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
<!-- Column 2: gear button with agent settings flyout -->
|
<!-- Column 2: gear button with agent settings flyout -->
|
||||||
<Button Grid.Column="2" Classes="icon-btn"
|
<Button Grid.Column="2" Classes="icon-btn"
|
||||||
ToolTip.Tip="{loc:Tr details.agentSettingsTip}"
|
ToolTip.Tip="{loc:Tr details.agentSettingsTip}"
|
||||||
IsEnabled="{Binding IsAgentSectionEnabled}"
|
IsEnabled="{Binding AgentSettings.IsAgentSectionEnabled}"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Margin="6,0,0,0">
|
Margin="6,0,0,0">
|
||||||
<TextBlock Text="⚙" FontSize="{StaticResource FontSizeTaskTitle}"/>
|
<TextBlock Text="⚙" FontSize="{StaticResource FontSizeTaskTitle}"/>
|
||||||
@@ -64,50 +64,50 @@
|
|||||||
<StackPanel Spacing="2">
|
<StackPanel Spacing="2">
|
||||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
|
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
|
||||||
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr details.modelLabel}" VerticalAlignment="Center"/>
|
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr details.modelLabel}" VerticalAlignment="Center"/>
|
||||||
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding ModelBadge}"/>
|
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding AgentSettings.ModelBadge}"/>
|
||||||
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
|
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
|
||||||
Command="{Binding ResetTaskModelCommand}"/>
|
Command="{Binding AgentSettings.ResetTaskModelCommand}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<ComboBox ItemsSource="{Binding TaskModelOptions}"
|
<ComboBox ItemsSource="{Binding AgentSettings.TaskModelOptions}"
|
||||||
SelectedItem="{Binding TaskModelSelection, Mode=TwoWay}"
|
SelectedItem="{Binding AgentSettings.TaskModelSelection, Mode=TwoWay}"
|
||||||
PlaceholderText="{Binding ModelInheritedHint}"
|
PlaceholderText="{Binding AgentSettings.ModelInheritedHint}"
|
||||||
HorizontalAlignment="Stretch"/>
|
HorizontalAlignment="Stretch"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="2">
|
<StackPanel Spacing="2">
|
||||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
|
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
|
||||||
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr details.maxTurnsLabel}" VerticalAlignment="Center"/>
|
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr details.maxTurnsLabel}" VerticalAlignment="Center"/>
|
||||||
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding TurnsBadge}"/>
|
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding AgentSettings.TurnsBadge}"/>
|
||||||
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
|
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
|
||||||
Command="{Binding ResetTaskTurnsCommand}"/>
|
Command="{Binding AgentSettings.ResetTaskTurnsCommand}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<NumericUpDown Value="{Binding TaskMaxTurns, Mode=TwoWay}"
|
<NumericUpDown Value="{Binding AgentSettings.TaskMaxTurns, Mode=TwoWay}"
|
||||||
PlaceholderText="{Binding TurnsInheritedHint}"
|
PlaceholderText="{Binding AgentSettings.TurnsInheritedHint}"
|
||||||
Minimum="1" Maximum="200" Increment="1" FormatString="0"
|
Minimum="1" Maximum="200" Increment="1" FormatString="0"
|
||||||
HorizontalAlignment="Stretch"/>
|
HorizontalAlignment="Stretch"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="2">
|
<StackPanel Spacing="2">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr details.systemPromptLabel}"/>
|
<TextBlock Classes="field-label" Text="{loc:Tr details.systemPromptLabel}"/>
|
||||||
<TextBox Text="{Binding TaskSystemPrompt, Mode=TwoWay}"
|
<TextBox Text="{Binding AgentSettings.TaskSystemPrompt, Mode=TwoWay}"
|
||||||
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="70"/>
|
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="70"/>
|
||||||
<TextBlock Classes="meta" Opacity="0.6"
|
<TextBlock Classes="meta" Opacity="0.6"
|
||||||
Text="{loc:Tr details.systemPromptPrepended}"
|
Text="{loc:Tr details.systemPromptPrepended}"
|
||||||
IsVisible="{Binding EffectiveSystemPromptHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
IsVisible="{Binding AgentSettings.EffectiveSystemPromptHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
<TextBlock Classes="meta" Opacity="0.6" TextWrapping="Wrap"
|
<TextBlock Classes="meta" Opacity="0.6" TextWrapping="Wrap"
|
||||||
Text="{Binding EffectiveSystemPromptHint}"
|
Text="{Binding AgentSettings.EffectiveSystemPromptHint}"
|
||||||
IsVisible="{Binding EffectiveSystemPromptHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
IsVisible="{Binding AgentSettings.EffectiveSystemPromptHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="2">
|
<StackPanel Spacing="2">
|
||||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
|
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
|
||||||
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr details.agentFileLabel}" VerticalAlignment="Center"/>
|
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr details.agentFileLabel}" VerticalAlignment="Center"/>
|
||||||
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding AgentBadge}"/>
|
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding AgentSettings.AgentBadge}"/>
|
||||||
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
|
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
|
||||||
Command="{Binding ResetTaskAgentCommand}"/>
|
Command="{Binding AgentSettings.ResetTaskAgentCommand}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<ComboBox ItemsSource="{Binding TaskAgentOptions}"
|
<ComboBox ItemsSource="{Binding AgentSettings.TaskAgentOptions}"
|
||||||
SelectedItem="{Binding TaskSelectedAgent, Mode=TwoWay}"
|
SelectedItem="{Binding AgentSettings.TaskSelectedAgent, Mode=TwoWay}"
|
||||||
HorizontalAlignment="Stretch">
|
HorizontalAlignment="Stretch">
|
||||||
<ComboBox.ItemTemplate>
|
<ComboBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
|
|||||||
@@ -266,26 +266,26 @@
|
|||||||
|
|
||||||
<!-- Git: one Approve + merge cockpit -->
|
<!-- Git: one Approve + merge cockpit -->
|
||||||
<ScrollViewer IsVisible="{Binding IsGitTab}" Padding="14,10">
|
<ScrollViewer IsVisible="{Binding IsGitTab}" Padding="14,10">
|
||||||
<StackPanel Spacing="12" IsVisible="{Binding ShowMergeSection}">
|
<StackPanel Spacing="12" IsVisible="{Binding Merge.ShowMergeSection}">
|
||||||
<TextBlock Classes="section-label" Text="MERGE" />
|
<TextBlock Classes="section-label" Text="MERGE" />
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Classes="field-label" Text="Target branch" />
|
<TextBlock Classes="field-label" Text="Target branch" />
|
||||||
<ComboBox ItemsSource="{Binding MergeTargetBranches}"
|
<ComboBox ItemsSource="{Binding Merge.MergeTargetBranches}"
|
||||||
SelectedItem="{Binding SelectedMergeTarget, Mode=TwoWay}"
|
SelectedItem="{Binding Merge.SelectedMergeTarget, Mode=TwoWay}"
|
||||||
HorizontalAlignment="Stretch" />
|
HorizontalAlignment="Stretch" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="0">
|
<StackPanel Spacing="0">
|
||||||
<TextBlock Classes="meta" Text="{Binding MergePreviewText}" TextWrapping="Wrap"
|
<TextBlock Classes="meta" Text="{Binding Merge.MergePreviewText}" TextWrapping="Wrap"
|
||||||
Foreground="{DynamicResource MossBrush}"
|
Foreground="{DynamicResource MossBrush}"
|
||||||
IsVisible="{Binding MergeIsClean}" />
|
IsVisible="{Binding Merge.MergeIsClean}" />
|
||||||
<TextBlock Classes="meta" Text="{Binding MergePreviewText}" TextWrapping="Wrap"
|
<TextBlock Classes="meta" Text="{Binding Merge.MergePreviewText}" TextWrapping="Wrap"
|
||||||
Foreground="{DynamicResource BloodBrush}"
|
Foreground="{DynamicResource BloodBrush}"
|
||||||
IsVisible="{Binding MergeIsConflict}" />
|
IsVisible="{Binding Merge.MergeIsConflict}" />
|
||||||
<TextBlock Classes="meta" Text="{Binding MergePreviewText}" TextWrapping="Wrap"
|
<TextBlock Classes="meta" Text="{Binding Merge.MergePreviewText}" TextWrapping="Wrap"
|
||||||
Foreground="{DynamicResource TextMuteBrush}"
|
Foreground="{DynamicResource TextMuteBrush}"
|
||||||
IsVisible="{Binding ShowMergePreviewMuted}" />
|
IsVisible="{Binding Merge.ShowMergePreviewMuted}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Primary action: Approve flows straight into the merge. -->
|
<!-- Primary action: Approve flows straight into the merge. -->
|
||||||
@@ -294,17 +294,17 @@
|
|||||||
Command="{Binding ApproveReviewCommand}"
|
Command="{Binding ApproveReviewCommand}"
|
||||||
IsVisible="{Binding IsWaitingForReview}" />
|
IsVisible="{Binding IsWaitingForReview}" />
|
||||||
<Button Classes="btn" Content="Open Diff" Margin="0,0,8,8"
|
<Button Classes="btn" Content="Open Diff" Margin="0,0,8,8"
|
||||||
Command="{Binding OpenDiffCommand}" />
|
Command="{Binding Merge.OpenDiffCommand}" />
|
||||||
<Button Classes="btn" Margin="0,0,8,8"
|
<Button Classes="btn" Margin="0,0,8,8"
|
||||||
ToolTip.Tip="{loc:Tr agent.openWorktreeTip}"
|
ToolTip.Tip="{loc:Tr agent.openWorktreeTip}"
|
||||||
Command="{Binding OpenWorktreeCommand}">
|
Command="{Binding Merge.OpenWorktreeCommand}">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
<TextBlock Text="Worktree" />
|
<TextBlock Text="Worktree" />
|
||||||
<PathIcon Data="{StaticResource Icon.ArrowOut}" Width="11" Height="11" />
|
<PathIcon Data="{StaticResource Icon.ArrowOut}" Width="11" Height="11" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Classes="btn" Content="Review Combined Diff" Margin="0,0,8,8"
|
<Button Classes="btn" Content="Review Combined Diff" Margin="0,0,8,8"
|
||||||
Command="{Binding ReviewCombinedDiffCommand}" />
|
Command="{Binding Merge.ReviewCombinedDiffCommand}" />
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|||||||
@@ -104,16 +104,16 @@
|
|||||||
<DockPanel>
|
<DockPanel>
|
||||||
<Border DockPanel.Dock="Top" Padding="12,8">
|
<Border DockPanel.Dock="Top" Padding="12,8">
|
||||||
<Button Classes="btn primary"
|
<Button Classes="btn primary"
|
||||||
Command="{Binding PlanDayCommand}"
|
Command="{Binding Prep.PlanDayCommand}"
|
||||||
IsEnabled="{Binding !IsPrepRunning}"
|
IsEnabled="{Binding !Prep.IsPrepRunning}"
|
||||||
Content="{loc:Tr details.planDay}"/>
|
Content="{loc:Tr details.planDay}"/>
|
||||||
</Border>
|
</Border>
|
||||||
<Panel>
|
<Panel>
|
||||||
<islands:SessionTerminalView
|
<islands:SessionTerminalView
|
||||||
Margin="18,8,18,0"
|
Margin="18,8,18,0"
|
||||||
Entries="{Binding PrepLog}" Label="daily-prep"
|
Entries="{Binding Prep.PrepLog}" Label="daily-prep"
|
||||||
IsRunning="{Binding IsPrepRunning}"/>
|
IsRunning="{Binding Prep.IsPrepRunning}"/>
|
||||||
<TextBlock IsVisible="{Binding ShowPrepEmptyState}"
|
<TextBlock IsVisible="{Binding Prep.ShowPrepEmptyState}"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
Foreground="{DynamicResource TextMuteBrush}"
|
Foreground="{DynamicResource TextMuteBrush}"
|
||||||
Text="{loc:Tr details.prepEmpty}"/>
|
Text="{loc:Tr details.prepEmpty}"/>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ public partial class DetailsIslandView : UserControl
|
|||||||
vm.PropertyChanged += OnViewModelPropertyChanged;
|
vm.PropertyChanged += OnViewModelPropertyChanged;
|
||||||
ApplyResizeStateForCurrentTask();
|
ApplyResizeStateForCurrentTask();
|
||||||
|
|
||||||
vm.ShowDiffModal = async (diffVm) =>
|
vm.Merge.ShowDiffModal = async (diffVm) =>
|
||||||
{
|
{
|
||||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
if (owner == null) return;
|
if (owner == null) return;
|
||||||
@@ -56,7 +56,7 @@ public partial class DetailsIslandView : UserControl
|
|||||||
await modal.ShowDialog(owner);
|
await modal.ShowDialog(owner);
|
||||||
};
|
};
|
||||||
|
|
||||||
vm.ShowMergeModal = async (mergeVm) =>
|
vm.Merge.ShowMergeModal = async (mergeVm) =>
|
||||||
{
|
{
|
||||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
if (owner == null) return;
|
if (owner == null) return;
|
||||||
@@ -64,7 +64,7 @@ public partial class DetailsIslandView : UserControl
|
|||||||
await modal.ShowDialog(owner);
|
await modal.ShowDialog(owner);
|
||||||
};
|
};
|
||||||
|
|
||||||
vm.ShowPlanningDiffModal = async (planningDiffVm) =>
|
vm.Merge.ShowPlanningDiffModal = async (planningDiffVm) =>
|
||||||
{
|
{
|
||||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
if (owner == null) return;
|
if (owner == null) return;
|
||||||
|
|||||||
@@ -77,11 +77,11 @@ public class DetailsIslandConflictSeamTests : IDisposable
|
|||||||
|
|
||||||
var vm = BuildVm(new ConflictApproveWorkerClient());
|
var vm = BuildVm(new ConflictApproveWorkerClient());
|
||||||
vm.Bind(new TaskRowViewModel { Id = taskId, Status = TaskStatus.WaitingForReview });
|
vm.Bind(new TaskRowViewModel { Id = taskId, Status = TaskStatus.WaitingForReview });
|
||||||
vm.SelectedMergeTarget = "main";
|
vm.Merge.SelectedMergeTarget = "main";
|
||||||
|
|
||||||
string? capturedTaskId = null;
|
string? capturedTaskId = null;
|
||||||
string? capturedTarget = null;
|
string? capturedTarget = null;
|
||||||
vm.RequestConflictResolution = (tid, target) =>
|
vm.Merge.RequestConflictResolution = (tid, target) =>
|
||||||
{
|
{
|
||||||
capturedTaskId = tid;
|
capturedTaskId = tid;
|
||||||
capturedTarget = target;
|
capturedTarget = target;
|
||||||
|
|||||||
@@ -140,11 +140,11 @@ public class DetailsIslandPlanningTests : IDisposable
|
|||||||
|
|
||||||
// Wait for the background load to settle
|
// Wait for the background load to settle
|
||||||
var deadline = DateTime.UtcNow.AddSeconds(5);
|
var deadline = DateTime.UtcNow.AddSeconds(5);
|
||||||
while (DateTime.UtcNow < deadline && vm.MergeTargetBranches.Count == 0)
|
while (DateTime.UtcNow < deadline && vm.Merge.MergeTargetBranches.Count == 0)
|
||||||
await Task.Delay(20);
|
await Task.Delay(20);
|
||||||
|
|
||||||
Assert.Contains("main", vm.MergeTargetBranches);
|
Assert.Contains("main", vm.Merge.MergeTargetBranches);
|
||||||
Assert.Contains("dev", vm.MergeTargetBranches);
|
Assert.Contains("dev", vm.Merge.MergeTargetBranches);
|
||||||
Assert.Equal("main", vm.SelectedMergeTarget);
|
Assert.Equal("main", vm.Merge.SelectedMergeTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public class DetailsIslandPrepModeTests : IDisposable
|
|||||||
|
|
||||||
stub.RaisePrepLine("{\"type\":\"assistant\",\"message\":{\"content\":[{\"type\":\"text\",\"text\":\"hi\"}]}}");
|
stub.RaisePrepLine("{\"type\":\"assistant\",\"message\":{\"content\":[{\"type\":\"text\",\"text\":\"hi\"}]}}");
|
||||||
|
|
||||||
Assert.NotEmpty(vm.PrepLog);
|
Assert.NotEmpty(vm.Prep.PrepLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -88,9 +88,9 @@ public class DetailsIslandPrepModeTests : IDisposable
|
|||||||
var vm = NewDetailsVm(stub);
|
var vm = NewDetailsVm(stub);
|
||||||
|
|
||||||
vm.ShowPrep();
|
vm.ShowPrep();
|
||||||
await vm.LoadLastPrepLogIfEmptyAsync();
|
await vm.Prep.LoadLastPrepLogIfEmptyAsync();
|
||||||
|
|
||||||
Assert.NotEmpty(vm.PrepLog);
|
Assert.NotEmpty(vm.Prep.PrepLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -98,7 +98,7 @@ public class DetailsIslandPrepModeTests : IDisposable
|
|||||||
{
|
{
|
||||||
var stub = new DefaultStub();
|
var stub = new DefaultStub();
|
||||||
var vm = NewDetailsVm(stub);
|
var vm = NewDetailsVm(stub);
|
||||||
await vm.PlanDayCommand.ExecuteAsync(null);
|
await vm.Prep.PlanDayCommand.ExecuteAsync(null);
|
||||||
Assert.Equal(1, stub.RunDailyPrepNowCalls);
|
Assert.Equal(1, stub.RunDailyPrepNowCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +106,6 @@ public class DetailsIslandPrepModeTests : IDisposable
|
|||||||
public void ShowPrepEmptyState_true_when_empty_and_not_running()
|
public void ShowPrepEmptyState_true_when_empty_and_not_running()
|
||||||
{
|
{
|
||||||
var vm = NewDetailsVm(new DefaultStub());
|
var vm = NewDetailsVm(new DefaultStub());
|
||||||
Assert.True(vm.ShowPrepEmptyState);
|
Assert.True(vm.Prep.ShowPrepEmptyState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user