feat(ui): extract TaskMonitorViewModel streaming core; DetailsIsland delegates
This commit is contained in:
@@ -1,11 +1,9 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Text;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
using ClaudeDo.Data.Models;
|
using ClaudeDo.Data.Models;
|
||||||
using ClaudeDo.Data.Repositories;
|
using ClaudeDo.Data.Repositories;
|
||||||
using ClaudeDo.Ui.Helpers;
|
|
||||||
using ClaudeDo.Ui.Localization;
|
using ClaudeDo.Ui.Localization;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.Services.Interfaces;
|
using ClaudeDo.Ui.Services.Interfaces;
|
||||||
@@ -148,99 +146,53 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
public string DiffAddText => $"+{DiffAdditions}";
|
public string DiffAddText => $"+{DiffAdditions}";
|
||||||
public string DiffDelText => $"-{DiffDeletions}";
|
public string DiffDelText => $"-{DiffDeletions}";
|
||||||
|
|
||||||
public bool ShowRoadblock => IsFailed;
|
// ── Monitor forwarding ───────────────────────────────────────────────────
|
||||||
public string RoadblockMessage =>
|
public TaskMonitorViewModel Monitor { get; }
|
||||||
IsFailed ? "The session ended with an error." : "";
|
|
||||||
|
|
||||||
[ObservableProperty]
|
public ObservableCollection<LogLineViewModel> Log => Monitor.Log;
|
||||||
[NotifyPropertyChangedFor(nameof(ShowSessionOutcome))]
|
|
||||||
private string? _sessionOutcome;
|
|
||||||
|
|
||||||
public bool ShowSessionOutcome =>
|
public string AgentState
|
||||||
!string.IsNullOrWhiteSpace(SessionOutcome)
|
|
||||||
&& (IsWaitingForReview || IsDone || IsFailed || IsCancelled);
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyPropertyChangedFor(nameof(ShowRoadblockCard))]
|
|
||||||
private string? _roadblocks;
|
|
||||||
|
|
||||||
public bool ShowRoadblockCard =>
|
|
||||||
!string.IsNullOrWhiteSpace(Roadblocks)
|
|
||||||
&& (IsWaitingForReview || IsDone || IsFailed || IsCancelled);
|
|
||||||
|
|
||||||
private const string RoadblockMarker = "Roadblocks reported during the run:";
|
|
||||||
|
|
||||||
private void ApplyOutcome(string? result, string? errorFallback)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(result))
|
get => Monitor.AgentState;
|
||||||
{
|
set => Monitor.AgentState = value;
|
||||||
SessionOutcome = errorFallback;
|
|
||||||
Roadblocks = null;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var idx = result.IndexOf(RoadblockMarker, StringComparison.Ordinal);
|
public string AgentStatusLabel => Monitor.AgentStatusLabel;
|
||||||
if (idx < 0)
|
public bool IsIdle => Monitor.IsIdle;
|
||||||
|
public bool IsQueued => Monitor.IsQueued;
|
||||||
|
public bool IsRunning => Monitor.IsRunning;
|
||||||
|
public bool IsWaitingForReview => Monitor.IsWaitingForReview;
|
||||||
|
public bool IsWaitingForChildren => Monitor.IsWaitingForChildren;
|
||||||
|
public bool IsDone => Monitor.IsDone;
|
||||||
|
public bool IsFailed => Monitor.IsFailed;
|
||||||
|
public bool IsCancelled => Monitor.IsCancelled;
|
||||||
|
public bool ShowContinue => Monitor.ShowContinue;
|
||||||
|
public bool ShowResetAndRetry => Monitor.ShowResetAndRetry;
|
||||||
|
public bool ShowRoadblock => Monitor.ShowRoadblock;
|
||||||
|
public string RoadblockMessage => Monitor.RoadblockMessage;
|
||||||
|
public bool ShowSessionOutcome => Monitor.ShowSessionOutcome;
|
||||||
|
public bool ShowRoadblockCard => Monitor.ShowRoadblockCard;
|
||||||
|
|
||||||
|
public string? SessionOutcome
|
||||||
{
|
{
|
||||||
SessionOutcome = result;
|
get => Monitor.SessionOutcome;
|
||||||
Roadblocks = null;
|
set => Monitor.SessionOutcome = value;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var summary = result[..idx].TrimEnd().TrimEnd('⚠').TrimEnd();
|
public string? Roadblocks
|
||||||
SessionOutcome = string.IsNullOrWhiteSpace(summary) ? null : summary;
|
{
|
||||||
Roadblocks = result[(idx + RoadblockMarker.Length)..].Trim();
|
get => Monitor.Roadblocks;
|
||||||
|
set => Monitor.Roadblocks = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string SessionLabel => "claude-session";
|
public string SessionLabel => "claude-session";
|
||||||
|
|
||||||
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]
|
|
||||||
[NotifyCanExecuteChangedFor(nameof(EnqueueCommand))]
|
|
||||||
[NotifyCanExecuteChangedFor(nameof(DequeueCommand))]
|
|
||||||
[NotifyCanExecuteChangedFor(nameof(ResetAndRetryCommand))]
|
|
||||||
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
|
|
||||||
private string _agentState = "idle";
|
|
||||||
public string AgentStatusLabel => Loc.T($"vm.agentStatus.{AgentState}");
|
|
||||||
public bool IsIdle => AgentState == "idle";
|
|
||||||
public bool IsQueued => AgentState == "queued";
|
|
||||||
public bool IsRunning => AgentState == "running";
|
|
||||||
public bool IsWaitingForReview => AgentState == "review";
|
|
||||||
public bool IsWaitingForChildren => AgentState == "children";
|
|
||||||
public bool IsDone => AgentState == "done";
|
|
||||||
public bool IsFailed => AgentState == "failed";
|
|
||||||
public bool IsCancelled => AgentState == "cancelled";
|
|
||||||
|
|
||||||
public bool ShowContinue => IsFailed || IsCancelled;
|
|
||||||
public bool ShowResetAndRetry => IsFailed || IsCancelled || IsDone;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
|
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
|
||||||
private string? _latestRunSessionId;
|
private string? _latestRunSessionId;
|
||||||
|
|
||||||
partial void OnAgentStateChanged(string value)
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(AgentStatusLabel));
|
|
||||||
OnPropertyChanged(nameof(IsIdle));
|
|
||||||
OnPropertyChanged(nameof(IsQueued));
|
|
||||||
OnPropertyChanged(nameof(IsRunning));
|
|
||||||
OnPropertyChanged(nameof(IsWaitingForReview));
|
|
||||||
OnPropertyChanged(nameof(IsWaitingForChildren));
|
|
||||||
OnPropertyChanged(nameof(IsDone));
|
|
||||||
OnPropertyChanged(nameof(IsFailed));
|
|
||||||
OnPropertyChanged(nameof(IsCancelled));
|
|
||||||
OnPropertyChanged(nameof(ShowContinue));
|
|
||||||
OnPropertyChanged(nameof(ShowResetAndRetry));
|
|
||||||
OnPropertyChanged(nameof(ShowRoadblock));
|
|
||||||
OnPropertyChanged(nameof(RoadblockMessage));
|
|
||||||
OnPropertyChanged(nameof(ShowSessionOutcome));
|
|
||||||
OnPropertyChanged(nameof(ShowRoadblockCard));
|
|
||||||
AgentSettings.IsRunning = IsRunning;
|
|
||||||
NotifySessionSections();
|
|
||||||
OnPropertyChanged(nameof(CanAcceptDrop));
|
|
||||||
}
|
|
||||||
|
|
||||||
[ObservableProperty] private string? _model;
|
[ObservableProperty] private string? _model;
|
||||||
|
|
||||||
[ObservableProperty] private string? _worktreePath;
|
[ObservableProperty] private string? _worktreePath;
|
||||||
@@ -272,7 +224,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<LogLineViewModel> Log { get; } = new();
|
|
||||||
public ObservableCollection<SubtaskRowViewModel> Subtasks { get; } = new();
|
public ObservableCollection<SubtaskRowViewModel> Subtasks { get; } = new();
|
||||||
public ObservableCollection<ChildOutcomeRowViewModel> ChildOutcomes { get; } = new();
|
public ObservableCollection<ChildOutcomeRowViewModel> ChildOutcomes { get; } = new();
|
||||||
public ObservableCollection<AttachmentRowViewModel> Attachments { get; } = new();
|
public ObservableCollection<AttachmentRowViewModel> Attachments { get; } = new();
|
||||||
@@ -302,11 +253,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
[ObservableProperty] private string _newSubtaskTitle = "";
|
[ObservableProperty] private string _newSubtaskTitle = "";
|
||||||
|
|
||||||
// Claude CLI stream-json parser + buffer for partial text deltas
|
|
||||||
private readonly StreamLineFormatter _formatter = new();
|
|
||||||
private readonly StringBuilder _claudeBuf = new();
|
|
||||||
|
|
||||||
private string? _subscribedTaskId;
|
|
||||||
private CancellationTokenSource? _loadCts;
|
private CancellationTokenSource? _loadCts;
|
||||||
|
|
||||||
private bool _suppressDescSave;
|
private bool _suppressDescSave;
|
||||||
@@ -332,56 +278,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
public bool HasReviewFeedback => !string.IsNullOrWhiteSpace(ReviewFeedback);
|
public bool HasReviewFeedback => !string.IsNullOrWhiteSpace(ReviewFeedback);
|
||||||
|
|
||||||
private static string StatusToStateKey(ClaudeDo.Data.Models.TaskStatus status) => status switch
|
|
||||||
{
|
|
||||||
ClaudeDo.Data.Models.TaskStatus.Queued => "queued",
|
|
||||||
ClaudeDo.Data.Models.TaskStatus.Running => "running",
|
|
||||||
ClaudeDo.Data.Models.TaskStatus.WaitingForReview => "review",
|
|
||||||
ClaudeDo.Data.Models.TaskStatus.WaitingForChildren => "children",
|
|
||||||
ClaudeDo.Data.Models.TaskStatus.Done => "done",
|
|
||||||
ClaudeDo.Data.Models.TaskStatus.Failed => "failed",
|
|
||||||
ClaudeDo.Data.Models.TaskStatus.Cancelled => "cancelled",
|
|
||||||
_ => "idle",
|
|
||||||
};
|
|
||||||
|
|
||||||
private static string FinishedStatusToStateKey(string status) => status switch
|
|
||||||
{
|
|
||||||
"done" => "done",
|
|
||||||
"failed" => "failed",
|
|
||||||
"cancelled" => "cancelled",
|
|
||||||
"waiting_for_review" => "review",
|
|
||||||
"waiting_for_children" => "children",
|
|
||||||
_ => status.ToLowerInvariant(),
|
|
||||||
};
|
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task RefreshStatusAsync(string taskId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
|
||||||
var entity = await ctx.Tasks
|
|
||||||
.AsNoTracking()
|
|
||||||
.FirstOrDefaultAsync(t => t.Id == taskId);
|
|
||||||
if (entity is null || Task?.Id != taskId) return;
|
|
||||||
|
|
||||||
AgentState = StatusToStateKey(entity.Status);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task RefreshOutcomeAsync(string taskId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
|
||||||
var entity = await ctx.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId);
|
|
||||||
var latestRun = await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId);
|
|
||||||
if (Task?.Id != taskId) return;
|
|
||||||
ApplyOutcome(entity?.Result, latestRun?.ErrorMarkdown);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public DetailsIslandViewModel(
|
public DetailsIslandViewModel(
|
||||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||||
IWorkerClient worker,
|
IWorkerClient worker,
|
||||||
@@ -395,6 +291,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
_notesApi = notesApi;
|
_notesApi = notesApi;
|
||||||
_merge = merge;
|
_merge = merge;
|
||||||
|
|
||||||
|
Monitor = new TaskMonitorViewModel(dbFactory, worker);
|
||||||
|
Monitor.PropertyChanged += OnMonitorPropertyChanged;
|
||||||
|
|
||||||
AgentSettings = new AgentConfigEditorViewModel(worker, AgentConfigScope.Task);
|
AgentSettings = new AgentConfigEditorViewModel(worker, AgentConfigScope.Task);
|
||||||
Merge = new MergeSectionViewModel(worker, services);
|
Merge = new MergeSectionViewModel(worker, services);
|
||||||
Prep = new PrepPanelViewModel(worker);
|
Prep = new PrepPanelViewModel(worker);
|
||||||
@@ -413,8 +312,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
_langChangedHandler = (_, _) => OnPropertyChanged(nameof(AgentStatusLabel));
|
_langChangedHandler = (_, _) => OnPropertyChanged(nameof(AgentStatusLabel));
|
||||||
Loc.LanguageChanged += _langChangedHandler;
|
Loc.LanguageChanged += _langChangedHandler;
|
||||||
|
|
||||||
_worker.TaskMessageEvent += OnTaskMessage;
|
|
||||||
|
|
||||||
_workerPropertyChangedHandler = (_, e) =>
|
_workerPropertyChangedHandler = (_, e) =>
|
||||||
{
|
{
|
||||||
if (e.PropertyName == nameof(IWorkerClient.IsConnected))
|
if (e.PropertyName == nameof(IWorkerClient.IsConnected))
|
||||||
@@ -429,7 +326,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
_workerTaskStartedHandler = (slot, taskId, startedAt) =>
|
_workerTaskStartedHandler = (slot, taskId, startedAt) =>
|
||||||
{
|
{
|
||||||
if (Task?.Id == taskId) AgentState = "running";
|
|
||||||
_ = RefreshChildOutcomeAsync(taskId);
|
_ = RefreshChildOutcomeAsync(taskId);
|
||||||
};
|
};
|
||||||
_worker.TaskStartedEvent += _workerTaskStartedHandler;
|
_worker.TaskStartedEvent += _workerTaskStartedHandler;
|
||||||
@@ -437,16 +333,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
_workerTaskFinishedHandler = (slot, taskId, status, finishedAt) =>
|
_workerTaskFinishedHandler = (slot, taskId, status, finishedAt) =>
|
||||||
{
|
{
|
||||||
if (Task?.Id != taskId) return;
|
if (Task?.Id != taskId) return;
|
||||||
FlushClaudeBuffer();
|
|
||||||
Log.Add(new LogLineViewModel
|
|
||||||
{
|
|
||||||
Kind = LogKind.Done,
|
|
||||||
Text = $"── {status.ToUpperInvariant()} · {finishedAt.ToLocalTime():HH:mm:ss} ──",
|
|
||||||
});
|
|
||||||
AgentState = FinishedStatusToStateKey(status);
|
|
||||||
_ = RefreshWorktreeAsync(taskId);
|
_ = RefreshWorktreeAsync(taskId);
|
||||||
_ = RefreshChildOutcomeAsync(taskId);
|
_ = RefreshChildOutcomeAsync(taskId);
|
||||||
_ = RefreshOutcomeAsync(taskId);
|
|
||||||
};
|
};
|
||||||
_worker.TaskFinishedEvent += _workerTaskFinishedHandler;
|
_worker.TaskFinishedEvent += _workerTaskFinishedHandler;
|
||||||
|
|
||||||
@@ -460,7 +348,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
_workerTaskUpdatedHandler = taskId =>
|
_workerTaskUpdatedHandler = taskId =>
|
||||||
{
|
{
|
||||||
if (Task?.Id == taskId) _ = RefreshStatusAsync(taskId);
|
|
||||||
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
|
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
|
||||||
_ = RefreshChildOutcomeAsync(taskId);
|
_ = RefreshChildOutcomeAsync(taskId);
|
||||||
};
|
};
|
||||||
@@ -475,64 +362,56 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
Monitor.PropertyChanged -= OnMonitorPropertyChanged;
|
||||||
|
Monitor.Dispose();
|
||||||
Loc.LanguageChanged -= _langChangedHandler;
|
Loc.LanguageChanged -= _langChangedHandler;
|
||||||
_worker.PropertyChanged -= _workerPropertyChangedHandler;
|
_worker.PropertyChanged -= _workerPropertyChangedHandler;
|
||||||
_worker.TaskStartedEvent -= _workerTaskStartedHandler;
|
_worker.TaskStartedEvent -= _workerTaskStartedHandler;
|
||||||
_worker.TaskFinishedEvent -= _workerTaskFinishedHandler;
|
_worker.TaskFinishedEvent -= _workerTaskFinishedHandler;
|
||||||
_worker.WorktreeUpdatedEvent -= _workerWorktreeUpdatedHandler;
|
_worker.WorktreeUpdatedEvent -= _workerWorktreeUpdatedHandler;
|
||||||
_worker.TaskUpdatedEvent -= _workerTaskUpdatedHandler;
|
_worker.TaskUpdatedEvent -= _workerTaskUpdatedHandler;
|
||||||
_worker.TaskMessageEvent -= OnTaskMessage;
|
|
||||||
AgentSettings.Dispose();
|
AgentSettings.Dispose();
|
||||||
Prep.Dispose();
|
Prep.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTaskMessage(string taskId, string line)
|
private void OnMonitorPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (taskId != _subscribedTaskId) return;
|
switch (e.PropertyName)
|
||||||
|
|
||||||
if (line.StartsWith("[stdout]", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
var body = line["[stdout]".Length..].TrimStart();
|
case nameof(TaskMonitorViewModel.AgentState):
|
||||||
AppendStdoutLine(body);
|
OnPropertyChanged(nameof(AgentState));
|
||||||
return;
|
OnPropertyChanged(nameof(AgentStatusLabel));
|
||||||
|
OnPropertyChanged(nameof(IsIdle));
|
||||||
|
OnPropertyChanged(nameof(IsQueued));
|
||||||
|
OnPropertyChanged(nameof(IsRunning));
|
||||||
|
OnPropertyChanged(nameof(IsWaitingForReview));
|
||||||
|
OnPropertyChanged(nameof(IsWaitingForChildren));
|
||||||
|
OnPropertyChanged(nameof(IsDone));
|
||||||
|
OnPropertyChanged(nameof(IsFailed));
|
||||||
|
OnPropertyChanged(nameof(IsCancelled));
|
||||||
|
OnPropertyChanged(nameof(ShowContinue));
|
||||||
|
OnPropertyChanged(nameof(ShowResetAndRetry));
|
||||||
|
OnPropertyChanged(nameof(ShowRoadblock));
|
||||||
|
OnPropertyChanged(nameof(RoadblockMessage));
|
||||||
|
OnPropertyChanged(nameof(ShowSessionOutcome));
|
||||||
|
OnPropertyChanged(nameof(ShowRoadblockCard));
|
||||||
|
EnqueueCommand.NotifyCanExecuteChanged();
|
||||||
|
DequeueCommand.NotifyCanExecuteChanged();
|
||||||
|
ResetAndRetryCommand.NotifyCanExecuteChanged();
|
||||||
|
ContinueCommand.NotifyCanExecuteChanged();
|
||||||
|
AgentSettings.IsRunning = IsRunning;
|
||||||
|
NotifySessionSections();
|
||||||
|
OnPropertyChanged(nameof(CanAcceptDrop));
|
||||||
|
break;
|
||||||
|
case nameof(TaskMonitorViewModel.SessionOutcome):
|
||||||
|
OnPropertyChanged(nameof(SessionOutcome));
|
||||||
|
OnPropertyChanged(nameof(ShowSessionOutcome));
|
||||||
|
break;
|
||||||
|
case nameof(TaskMonitorViewModel.Roadblocks):
|
||||||
|
OnPropertyChanged(nameof(Roadblocks));
|
||||||
|
OnPropertyChanged(nameof(ShowRoadblockCard));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
FlushClaudeBuffer();
|
|
||||||
|
|
||||||
var kind = line.StartsWith("[sys]", StringComparison.OrdinalIgnoreCase) ? LogKind.Sys
|
|
||||||
: line.StartsWith("[tool]", StringComparison.OrdinalIgnoreCase) ? LogKind.Tool
|
|
||||||
: line.StartsWith("[claude]", StringComparison.OrdinalIgnoreCase) ? LogKind.Claude
|
|
||||||
: line.StartsWith("[stderr]", StringComparison.OrdinalIgnoreCase) ? LogKind.Stderr
|
|
||||||
: line.StartsWith("[done]", StringComparison.OrdinalIgnoreCase) ? LogKind.Done
|
|
||||||
: LogKind.Msg;
|
|
||||||
Log.Add(new LogLineViewModel { Kind = kind, Text = line });
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AppendStdoutLine(string line)
|
|
||||||
{
|
|
||||||
var formatted = _formatter.FormatLine(line);
|
|
||||||
if (formatted is null) return;
|
|
||||||
_claudeBuf.Append(formatted);
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var text = _claudeBuf.ToString();
|
|
||||||
var nl = text.IndexOf('\n');
|
|
||||||
if (nl < 0) break;
|
|
||||||
var piece = text[..nl].TrimEnd('\r');
|
|
||||||
if (!string.IsNullOrWhiteSpace(piece))
|
|
||||||
Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
|
|
||||||
_claudeBuf.Clear();
|
|
||||||
_claudeBuf.Append(text[(nl + 1)..]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FlushClaudeBuffer()
|
|
||||||
{
|
|
||||||
if (_claudeBuf.Length == 0) return;
|
|
||||||
var piece = _claudeBuf.ToString().TrimEnd();
|
|
||||||
_claudeBuf.Clear();
|
|
||||||
if (!string.IsNullOrWhiteSpace(piece))
|
|
||||||
Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnEditableDescriptionChanged(string value)
|
partial void OnEditableDescriptionChanged(string value)
|
||||||
@@ -587,20 +466,16 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
Task = row;
|
Task = row;
|
||||||
OnPropertyChanged(nameof(TaskIdBadge));
|
OnPropertyChanged(nameof(TaskIdBadge));
|
||||||
Log.Clear();
|
Monitor.Reset();
|
||||||
Subtasks.Clear();
|
Subtasks.Clear();
|
||||||
ChildOutcomes.Clear();
|
ChildOutcomes.Clear();
|
||||||
Attachments.Clear();
|
Attachments.Clear();
|
||||||
DropStatus = null;
|
DropStatus = null;
|
||||||
OnPropertyChanged(nameof(HasChildOutcomes));
|
OnPropertyChanged(nameof(HasChildOutcomes));
|
||||||
SessionOutcome = null;
|
|
||||||
Roadblocks = null;
|
|
||||||
_claudeBuf.Clear();
|
|
||||||
Merge.Clear();
|
Merge.Clear();
|
||||||
|
|
||||||
if (row == null)
|
if (row == null)
|
||||||
{
|
{
|
||||||
_subscribedTaskId = null;
|
|
||||||
EditableTitle = "";
|
EditableTitle = "";
|
||||||
EditableDescription = "";
|
EditableDescription = "";
|
||||||
Model = null;
|
Model = null;
|
||||||
@@ -611,7 +486,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
BranchLine = null;
|
BranchLine = null;
|
||||||
DiffAdditions = 0;
|
DiffAdditions = 0;
|
||||||
DiffDeletions = 0;
|
DiffDeletions = 0;
|
||||||
AgentState = "idle";
|
|
||||||
LatestRunSessionId = null;
|
LatestRunSessionId = null;
|
||||||
AgentSettings.Clear();
|
AgentSettings.Clear();
|
||||||
return;
|
return;
|
||||||
@@ -649,7 +523,7 @@ 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;
|
||||||
AgentState = StatusToStateKey(entity.Status);
|
Monitor.ApplyState(entity.Status);
|
||||||
|
|
||||||
Merge.SyncTaskContext(row.Id, row.Title, row.IsPlanningParent);
|
Merge.SyncTaskContext(row.Id, row.Title, row.IsPlanningParent);
|
||||||
Merge.SyncWorktree(WorktreePath, WorktreeBaseCommit, WorktreeHeadCommit,
|
Merge.SyncWorktree(WorktreePath, WorktreeBaseCommit, WorktreeHeadCommit,
|
||||||
@@ -662,11 +536,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
var latestRun = await runRepo.GetLatestByTaskIdAsync(row.Id, ct);
|
var latestRun = await runRepo.GetLatestByTaskIdAsync(row.Id, ct);
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
LatestRunSessionId = latestRun?.SessionId;
|
LatestRunSessionId = latestRun?.SessionId;
|
||||||
ApplyOutcome(entity.Result, latestRun?.ErrorMarkdown);
|
Monitor.ApplyOutcome(entity.Result, latestRun?.ErrorMarkdown);
|
||||||
|
|
||||||
_subscribedTaskId = row.Id;
|
Monitor.SetTaskId(row.Id);
|
||||||
|
|
||||||
await ReplayLogFileAsync(entity.LogPath, ct);
|
await Monitor.ReplayLogFileAsync(entity.LogPath, ct);
|
||||||
|
|
||||||
var subs = await subtaskRepo.GetByTaskIdAsync(row.Id);
|
var subs = await subtaskRepo.GetByTaskIdAsync(row.Id);
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
@@ -746,56 +620,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
catch { /* best-effort */ }
|
catch { /* best-effort */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task ReplayLogFileAsync(string? logPath, CancellationToken ct)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(logPath)) return;
|
|
||||||
var expanded = ExpandUserPath(logPath);
|
|
||||||
if (!System.IO.File.Exists(expanded)) return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const int maxLines = 2000;
|
|
||||||
string[] all;
|
|
||||||
await using (var fs = new System.IO.FileStream(
|
|
||||||
expanded,
|
|
||||||
System.IO.FileMode.Open,
|
|
||||||
System.IO.FileAccess.Read,
|
|
||||||
System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete))
|
|
||||||
using (var reader = new System.IO.StreamReader(fs))
|
|
||||||
{
|
|
||||||
var list = new List<string>();
|
|
||||||
while (await reader.ReadLineAsync(ct) is { } line)
|
|
||||||
list.Add(line);
|
|
||||||
all = list.ToArray();
|
|
||||||
}
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
var start = Math.Max(0, all.Length - maxLines);
|
|
||||||
for (int i = start; i < all.Length; i++)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
if (_subscribedTaskId is null) return;
|
|
||||||
var line = all[i];
|
|
||||||
var normalized = line.StartsWith("[", StringComparison.Ordinal) ? line : "[stdout] " + line;
|
|
||||||
OnTaskMessage(_subscribedTaskId, normalized);
|
|
||||||
}
|
|
||||||
FlushClaudeBuffer();
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) { throw; }
|
|
||||||
catch { /* best-effort replay */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ExpandUserPath(string path)
|
|
||||||
{
|
|
||||||
if (path.StartsWith("~/", StringComparison.Ordinal) || path.StartsWith("~\\", StringComparison.Ordinal))
|
|
||||||
return System.IO.Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
||||||
path[2..]);
|
|
||||||
if (path == "~")
|
|
||||||
return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task LoadPlanningChildrenAsync(string parentTaskId, CancellationToken ct)
|
private async System.Threading.Tasks.Task LoadPlanningChildrenAsync(string parentTaskId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -899,7 +723,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
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} ← main" : null;
|
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null;
|
||||||
AgentState = StatusToStateKey(entity.Status);
|
|
||||||
if (Task is { } row && entity.Worktree?.DiffStat is { } stat)
|
if (Task is { } row && entity.Worktree?.DiffStat is { } stat)
|
||||||
row.DiffStat = stat;
|
row.DiffStat = stat;
|
||||||
var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat);
|
var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat);
|
||||||
@@ -959,7 +782,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
? ClaudeDo.Data.Models.TaskStatus.Done
|
? ClaudeDo.Data.Models.TaskStatus.Done
|
||||||
: ClaudeDo.Data.Models.TaskStatus.Idle;
|
: ClaudeDo.Data.Models.TaskStatus.Idle;
|
||||||
Task.Status = entity.Status;
|
Task.Status = entity.Status;
|
||||||
AgentState = StatusToStateKey(entity.Status);
|
Monitor.ApplyState(entity.Status);
|
||||||
await repo.UpdateAsync(entity);
|
await repo.UpdateAsync(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
317
src/ClaudeDo.Ui/ViewModels/Islands/TaskMonitorViewModel.cs
Normal file
317
src/ClaudeDo.Ui/ViewModels/Islands/TaskMonitorViewModel.cs
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Text;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Data.Repositories;
|
||||||
|
using ClaudeDo.Ui.Helpers;
|
||||||
|
using ClaudeDo.Ui.Localization;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
|
public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
|
private readonly IWorkerClient _worker;
|
||||||
|
|
||||||
|
private readonly StreamLineFormatter _formatter = new();
|
||||||
|
private readonly StringBuilder _claudeBuf = new();
|
||||||
|
private string? _subscribedTaskId;
|
||||||
|
|
||||||
|
public string? SubscribedTaskId => _subscribedTaskId;
|
||||||
|
|
||||||
|
public ObservableCollection<LogLineViewModel> Log { get; } = new();
|
||||||
|
|
||||||
|
[ObservableProperty] private string _agentState = "idle";
|
||||||
|
|
||||||
|
public string AgentStatusLabel => Loc.T($"vm.agentStatus.{AgentState}");
|
||||||
|
public bool IsIdle => AgentState == "idle";
|
||||||
|
public bool IsQueued => AgentState == "queued";
|
||||||
|
public bool IsRunning => AgentState == "running";
|
||||||
|
public bool IsWaitingForReview => AgentState == "review";
|
||||||
|
public bool IsWaitingForChildren => AgentState == "children";
|
||||||
|
public bool IsDone => AgentState == "done";
|
||||||
|
public bool IsFailed => AgentState == "failed";
|
||||||
|
public bool IsCancelled => AgentState == "cancelled";
|
||||||
|
|
||||||
|
public bool ShowContinue => IsFailed || IsCancelled;
|
||||||
|
public bool ShowResetAndRetry => IsFailed || IsCancelled || IsDone;
|
||||||
|
|
||||||
|
public bool ShowRoadblock => IsFailed;
|
||||||
|
public string RoadblockMessage => IsFailed ? "The session ended with an error." : "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowSessionOutcome))]
|
||||||
|
private string? _sessionOutcome;
|
||||||
|
|
||||||
|
public bool ShowSessionOutcome =>
|
||||||
|
!string.IsNullOrWhiteSpace(SessionOutcome)
|
||||||
|
&& (IsWaitingForReview || IsDone || IsFailed || IsCancelled);
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowRoadblockCard))]
|
||||||
|
private string? _roadblocks;
|
||||||
|
|
||||||
|
public bool ShowRoadblockCard =>
|
||||||
|
!string.IsNullOrWhiteSpace(Roadblocks)
|
||||||
|
&& (IsWaitingForReview || IsDone || IsFailed || IsCancelled);
|
||||||
|
|
||||||
|
private const string RoadblockMarker = "Roadblocks reported during the run:";
|
||||||
|
|
||||||
|
// Captured handler delegates for disposal
|
||||||
|
private readonly Action<string, string> _onTaskMessage;
|
||||||
|
private readonly Action<string, string, DateTime> _onTaskStarted;
|
||||||
|
private readonly Action<string, string, string, DateTime> _onTaskFinished;
|
||||||
|
private readonly Action<string> _onTaskUpdated;
|
||||||
|
|
||||||
|
public TaskMonitorViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker)
|
||||||
|
{
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
_worker = worker;
|
||||||
|
|
||||||
|
_onTaskMessage = OnTaskMessage;
|
||||||
|
_worker.TaskMessageEvent += _onTaskMessage;
|
||||||
|
|
||||||
|
_onTaskStarted = (slot, taskId, startedAt) =>
|
||||||
|
{
|
||||||
|
if (taskId == _subscribedTaskId)
|
||||||
|
AgentState = "running";
|
||||||
|
};
|
||||||
|
_worker.TaskStartedEvent += _onTaskStarted;
|
||||||
|
|
||||||
|
_onTaskFinished = (slot, taskId, status, finishedAt) =>
|
||||||
|
{
|
||||||
|
if (taskId != _subscribedTaskId) return;
|
||||||
|
FlushClaudeBuffer();
|
||||||
|
Log.Add(new LogLineViewModel
|
||||||
|
{
|
||||||
|
Kind = LogKind.Done,
|
||||||
|
Text = $"── {status.ToUpperInvariant()} · {finishedAt.ToLocalTime():HH:mm:ss} ──",
|
||||||
|
});
|
||||||
|
AgentState = FinishedStatusToStateKey(status);
|
||||||
|
_ = RefreshOutcomeAsync(taskId);
|
||||||
|
};
|
||||||
|
_worker.TaskFinishedEvent += _onTaskFinished;
|
||||||
|
|
||||||
|
_onTaskUpdated = taskId =>
|
||||||
|
{
|
||||||
|
if (taskId == _subscribedTaskId)
|
||||||
|
_ = RefreshStatusAsync(taskId);
|
||||||
|
};
|
||||||
|
_worker.TaskUpdatedEvent += _onTaskUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnAgentStateChanged(string value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(AgentStatusLabel));
|
||||||
|
OnPropertyChanged(nameof(IsIdle));
|
||||||
|
OnPropertyChanged(nameof(IsQueued));
|
||||||
|
OnPropertyChanged(nameof(IsRunning));
|
||||||
|
OnPropertyChanged(nameof(IsWaitingForReview));
|
||||||
|
OnPropertyChanged(nameof(IsWaitingForChildren));
|
||||||
|
OnPropertyChanged(nameof(IsDone));
|
||||||
|
OnPropertyChanged(nameof(IsFailed));
|
||||||
|
OnPropertyChanged(nameof(IsCancelled));
|
||||||
|
OnPropertyChanged(nameof(ShowContinue));
|
||||||
|
OnPropertyChanged(nameof(ShowResetAndRetry));
|
||||||
|
OnPropertyChanged(nameof(ShowRoadblock));
|
||||||
|
OnPropertyChanged(nameof(RoadblockMessage));
|
||||||
|
OnPropertyChanged(nameof(ShowSessionOutcome));
|
||||||
|
OnPropertyChanged(nameof(ShowRoadblockCard));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Log.Clear();
|
||||||
|
_claudeBuf.Clear();
|
||||||
|
_subscribedTaskId = null;
|
||||||
|
AgentState = "idle";
|
||||||
|
SessionOutcome = null;
|
||||||
|
Roadblocks = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetTaskId(string id) => _subscribedTaskId = id;
|
||||||
|
|
||||||
|
public void ApplyState(ClaudeDo.Data.Models.TaskStatus status) =>
|
||||||
|
AgentState = StatusToStateKey(status);
|
||||||
|
|
||||||
|
public void ApplyOutcome(string? result, string? errorFallback)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(result))
|
||||||
|
{
|
||||||
|
SessionOutcome = errorFallback;
|
||||||
|
Roadblocks = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = result.IndexOf(RoadblockMarker, StringComparison.Ordinal);
|
||||||
|
if (idx < 0)
|
||||||
|
{
|
||||||
|
SessionOutcome = result;
|
||||||
|
Roadblocks = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary = result[..idx].TrimEnd().TrimEnd('⚠').TrimEnd();
|
||||||
|
SessionOutcome = string.IsNullOrWhiteSpace(summary) ? null : summary;
|
||||||
|
Roadblocks = result[(idx + RoadblockMarker.Length)..].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async System.Threading.Tasks.Task ReplayLogFileAsync(string? logPath, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(logPath)) return;
|
||||||
|
var expanded = ExpandUserPath(logPath);
|
||||||
|
if (!System.IO.File.Exists(expanded)) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const int maxLines = 2000;
|
||||||
|
string[] all;
|
||||||
|
await using (var fs = new System.IO.FileStream(
|
||||||
|
expanded,
|
||||||
|
System.IO.FileMode.Open,
|
||||||
|
System.IO.FileAccess.Read,
|
||||||
|
System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete))
|
||||||
|
using (var reader = new System.IO.StreamReader(fs))
|
||||||
|
{
|
||||||
|
var list = new List<string>();
|
||||||
|
while (await reader.ReadLineAsync(ct) is { } line)
|
||||||
|
list.Add(line);
|
||||||
|
all = list.ToArray();
|
||||||
|
}
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var start = Math.Max(0, all.Length - maxLines);
|
||||||
|
for (int i = start; i < all.Length; i++)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
if (_subscribedTaskId is null) return;
|
||||||
|
var line = all[i];
|
||||||
|
var normalized = line.StartsWith("[", StringComparison.Ordinal) ? line : "[stdout] " + line;
|
||||||
|
OnTaskMessage(_subscribedTaskId, normalized);
|
||||||
|
}
|
||||||
|
FlushClaudeBuffer();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { throw; }
|
||||||
|
catch { /* best-effort replay */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTaskMessage(string taskId, string line)
|
||||||
|
{
|
||||||
|
if (taskId != _subscribedTaskId) return;
|
||||||
|
|
||||||
|
if (line.StartsWith("[stdout]", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var body = line["[stdout]".Length..].TrimStart();
|
||||||
|
AppendStdoutLine(body);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlushClaudeBuffer();
|
||||||
|
|
||||||
|
var kind = line.StartsWith("[sys]", StringComparison.OrdinalIgnoreCase) ? LogKind.Sys
|
||||||
|
: line.StartsWith("[tool]", StringComparison.OrdinalIgnoreCase) ? LogKind.Tool
|
||||||
|
: line.StartsWith("[claude]", StringComparison.OrdinalIgnoreCase) ? LogKind.Claude
|
||||||
|
: line.StartsWith("[stderr]", StringComparison.OrdinalIgnoreCase) ? LogKind.Stderr
|
||||||
|
: line.StartsWith("[done]", StringComparison.OrdinalIgnoreCase) ? LogKind.Done
|
||||||
|
: LogKind.Msg;
|
||||||
|
Log.Add(new LogLineViewModel { Kind = kind, Text = line });
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendStdoutLine(string line)
|
||||||
|
{
|
||||||
|
var formatted = _formatter.FormatLine(line);
|
||||||
|
if (formatted is null) return;
|
||||||
|
_claudeBuf.Append(formatted);
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var text = _claudeBuf.ToString();
|
||||||
|
var nl = text.IndexOf('\n');
|
||||||
|
if (nl < 0) break;
|
||||||
|
var piece = text[..nl].TrimEnd('\r');
|
||||||
|
if (!string.IsNullOrWhiteSpace(piece))
|
||||||
|
Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
|
||||||
|
_claudeBuf.Clear();
|
||||||
|
_claudeBuf.Append(text[(nl + 1)..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FlushClaudeBuffer()
|
||||||
|
{
|
||||||
|
if (_claudeBuf.Length == 0) return;
|
||||||
|
var piece = _claudeBuf.ToString().TrimEnd();
|
||||||
|
_claudeBuf.Clear();
|
||||||
|
if (!string.IsNullOrWhiteSpace(piece))
|
||||||
|
Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async System.Threading.Tasks.Task RefreshStatusAsync(string taskId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var entity = await ctx.Tasks
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == taskId);
|
||||||
|
if (entity is null || _subscribedTaskId != taskId) return;
|
||||||
|
|
||||||
|
AgentState = StatusToStateKey(entity.Status);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async System.Threading.Tasks.Task RefreshOutcomeAsync(string taskId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var entity = await ctx.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId);
|
||||||
|
var latestRun = await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId);
|
||||||
|
if (_subscribedTaskId != taskId) return;
|
||||||
|
ApplyOutcome(entity?.Result, latestRun?.ErrorMarkdown);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string StatusToStateKey(ClaudeDo.Data.Models.TaskStatus status) => status switch
|
||||||
|
{
|
||||||
|
ClaudeDo.Data.Models.TaskStatus.Queued => "queued",
|
||||||
|
ClaudeDo.Data.Models.TaskStatus.Running => "running",
|
||||||
|
ClaudeDo.Data.Models.TaskStatus.WaitingForReview => "review",
|
||||||
|
ClaudeDo.Data.Models.TaskStatus.WaitingForChildren => "children",
|
||||||
|
ClaudeDo.Data.Models.TaskStatus.Done => "done",
|
||||||
|
ClaudeDo.Data.Models.TaskStatus.Failed => "failed",
|
||||||
|
ClaudeDo.Data.Models.TaskStatus.Cancelled => "cancelled",
|
||||||
|
_ => "idle",
|
||||||
|
};
|
||||||
|
|
||||||
|
internal static string FinishedStatusToStateKey(string status) => status switch
|
||||||
|
{
|
||||||
|
"done" => "done",
|
||||||
|
"failed" => "failed",
|
||||||
|
"cancelled" => "cancelled",
|
||||||
|
"waiting_for_review" => "review",
|
||||||
|
"waiting_for_children" => "children",
|
||||||
|
_ => status.ToLowerInvariant(),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string ExpandUserPath(string path)
|
||||||
|
{
|
||||||
|
if (path.StartsWith("~/", StringComparison.Ordinal) || path.StartsWith("~\\", StringComparison.Ordinal))
|
||||||
|
return System.IO.Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
path[2..]);
|
||||||
|
if (path == "~")
|
||||||
|
return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_worker.TaskMessageEvent -= _onTaskMessage;
|
||||||
|
_worker.TaskStartedEvent -= _onTaskStarted;
|
||||||
|
_worker.TaskFinishedEvent -= _onTaskFinished;
|
||||||
|
_worker.TaskUpdatedEvent -= _onTaskUpdated;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user