review_task/continue_merge on a planning parent always leaves conflicts in the tree, and the UI auto-opened the resolver on every PlanningMergeConflict broadcast regardless of who started the merge -- so a running Claude session resolving a unit-merge conflict could race a human editing the same shared checkout in a resolver window neither of them asked for. PlanningMergeOrchestrator.StartAsync now takes an externallyDriven flag (set by ExternalMcpService's MCP-driven review_task path, left false for the UI's ApproveReview) that rides along on the PlanningMergeConflict broadcast. The UI only auto-opens the resolver when it's false; otherwise it shows a persistent banner with a manual "Open resolver" button, cleared on PlanningMergeAborted/PlanningCompleted. A new GetActiveExternalConflictsAsync query (checked against GitService.IsMidMergeAsync rather than the in-memory flag alone) lets the UI resync the banner on reconnect instead of trusting a one-shot broadcast that isn't replayed after a restart. The childless single-task conflict path was checked and needed no change -- it only broadcasts the generic TaskUpdated, never PlanningMergeConflict.
171 lines
10 KiB
C#
171 lines
10 KiB
C#
using System.ComponentModel;
|
|
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Data.Repositories;
|
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
|
|
|
namespace ClaudeDo.Ui.Services;
|
|
|
|
public interface IWorkerClient : INotifyPropertyChanged
|
|
{
|
|
bool IsConnected { get; }
|
|
bool IsReconnecting { get; }
|
|
|
|
event Action<string, string, DateTime>? TaskStartedEvent;
|
|
event Action<string, string, string, DateTime>? TaskFinishedEvent;
|
|
event Action<string>? TaskUpdatedEvent;
|
|
/// <summary>Raised once when the SignalR connection is first established, and again on every reconnect.</summary>
|
|
event Action? ConnectionRestoredEvent;
|
|
event Action<string>? WorktreeUpdatedEvent;
|
|
event Action<string>? ListUpdatedEvent;
|
|
event Action<string, string>? TaskMessageEvent;
|
|
event Action<WorkerLogEntry>? WorkerLogReceivedEvent;
|
|
|
|
/// <summary>A running task raised a question via AskUser: (taskId, questionId, question).</summary>
|
|
event Action<string, string, string>? TaskQuestionAskedEvent;
|
|
/// <summary>A pending question was answered, timed out, or the run ended: (taskId, questionId).</summary>
|
|
event Action<string, string>? TaskQuestionResolvedEvent;
|
|
|
|
/// <summary>A running list-handler session called handoff_list_handler at the end of Phase 2:
|
|
/// (handlerTaskId, survivingTaskIds). The UI opens a second ConPTY tile for the same task.</summary>
|
|
event Action<string, IReadOnlyList<string>>? HandoffRequestedEvent;
|
|
|
|
event Action? PrepStartedEvent;
|
|
event Action<string>? PrepLineEvent;
|
|
event Action<bool>? PrepFinishedEvent;
|
|
|
|
event Action<string, string>? PlanningMergeStartedEvent;
|
|
event Action<string, string>? PlanningSubtaskMergedEvent;
|
|
/// <summary>(planningTaskId, subtaskId, conflictedFiles, externallyDriven). externallyDriven
|
|
/// is true when an MCP session (not the UI) started the unit merge — the resolver must not
|
|
/// auto-open in that case.</summary>
|
|
event Action<string, string, IReadOnlyList<string>, bool>? PlanningMergeConflictEvent;
|
|
event Action<string>? PlanningMergeAbortedEvent;
|
|
event Action<string>? PlanningCompletedEvent;
|
|
|
|
event Action<PrimeFiredEvent>? PrimeFired;
|
|
|
|
string? LastApproveTarget { get; }
|
|
|
|
IReadOnlyList<ActiveTask> GetActiveTasks();
|
|
|
|
Task WakeQueueAsync();
|
|
Task RunNowAsync(string taskId);
|
|
Task ContinueTaskAsync(string taskId, string followUpPrompt);
|
|
/// <summary>Answer a question a running task raised via AskUser.</summary>
|
|
Task AnswerTaskQuestionAsync(string taskId, string questionId, string answer);
|
|
/// <summary>The question a running task is currently blocked on, if any (for re-attach).</summary>
|
|
Task<PendingQuestionDto?> GetPendingQuestionAsync(string taskId);
|
|
Task ResetTaskAsync(string taskId);
|
|
Task CancelTaskAsync(string taskId);
|
|
Task<List<AgentInfo>> GetAgentsAsync();
|
|
Task RefreshAgentsAsync();
|
|
Task<SeedResultDto?> RestoreDefaultAgentsAsync();
|
|
Task<ListConfigDto?> GetListConfigAsync(string listId);
|
|
Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto);
|
|
/// <summary>Repo-import folders remembered for the "Add repos as lists" dialog. Throws on
|
|
/// a failed hub call — the caller surfaces the failure rather than showing an empty list.</summary>
|
|
Task<List<string>> GetRepoImportFoldersAsync();
|
|
/// <summary>Persists the repo-import folder list. Throws on a failed hub call.</summary>
|
|
Task SetRepoImportFoldersAsync(List<string> folders);
|
|
Task<List<SessionSkillDto>> GetSessionSkillsAsync();
|
|
Task<List<string>> InstallSessionSkillAsync(string url);
|
|
Task UpdateSessionSkillAsync(string sourceUrl);
|
|
Task RemoveSessionSkillAsync(string sourceUrl);
|
|
Task SetTaskStatusAsync(string taskId, TaskStatus status);
|
|
Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch);
|
|
Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch);
|
|
Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage);
|
|
Task RejectReviewToQueueAsync(string taskId, string feedback);
|
|
Task RejectReviewToIdleAsync(string taskId);
|
|
Task CancelReviewAsync(string taskId);
|
|
|
|
// ── Conflict resolution (worker hub side implemented by Layer C) ──
|
|
Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch);
|
|
Task<MergeConflictDocumentsDto> GetMergeConflictDocumentsAsync(string taskId);
|
|
Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent);
|
|
Task<MergeResultDto> ContinueConflictMergeAsync(string taskId);
|
|
Task AbortConflictMergeAsync(string taskId);
|
|
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
|
/// <summary>Commits an interactively-worked task's worktree and moves it to WaitingForReview
|
|
/// (the only path that flips a hand-driven ConPTY session into the review/merge pipeline).</summary>
|
|
Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default);
|
|
/// <summary>Launch spec for an embedded ConPTY terminal to open an interactive session
|
|
/// in a task's worktree (same worktree prep as an autonomous run).</summary>
|
|
Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default);
|
|
/// <summary>Launch spec for an ad-hoc interactive session in an arbitrary directory --
|
|
/// no task, no worktree.</summary>
|
|
Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default);
|
|
/// <summary>Launch spec for an embedded ConPTY "merge helper" session that drives the given
|
|
/// tasks to a merged/Done state. listId scopes the session (and cwd) to that list.</summary>
|
|
Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default);
|
|
/// <summary>Creates the ClaudeDo task that owns a list-handler run (one per run, Idle/IsManual,
|
|
/// never queued) so the ConPTY tile can be task-based instead of ad-hoc. Returns the new task id.</summary>
|
|
Task<string> CreateMergeHelperTaskAsync(
|
|
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default);
|
|
/// <summary>Launch spec for the fresh ConPTY session a merge-helper run hands off to once Phase 2
|
|
/// is done -- reuses the SAME handler task id (no new task created).</summary>
|
|
Task<LaunchSpec> GetMergeHelperHandoffLaunchSpecAsync(
|
|
string taskId, IReadOnlyList<string> survivingTaskIds, CancellationToken ct = default);
|
|
/// <summary>Starts a planning session and returns the launch spec for an embedded ConPTY
|
|
/// planning terminal (replaces StartPlanningSessionAsync's external wt window).</summary>
|
|
Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default);
|
|
/// <summary>Resumes a planning session and returns its embedded-ConPTY launch spec.</summary>
|
|
Task<LaunchSpec> GetPlanningResumeLaunchSpecAsync(string taskId, CancellationToken ct = default);
|
|
Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
|
Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default);
|
|
Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default);
|
|
Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct = default);
|
|
Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId);
|
|
Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId);
|
|
Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch);
|
|
Task ContinuePlanningMergeAsync(string planningTaskId);
|
|
Task AbortPlanningMergeAsync(string planningTaskId);
|
|
/// <summary>Unit merges currently paused on a conflict that an MCP session started. Called
|
|
/// on (re)connect to recover the "don't auto-open" banner state after a UI restart.</summary>
|
|
Task<IReadOnlyList<PlanningMergeConflictStateDto>> GetActiveExternalPlanningMergeConflictsAsync();
|
|
Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default);
|
|
Task<string?> GetWeekReportAsync(DateOnly start, DateOnly end);
|
|
Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end);
|
|
Task<bool> RunDailyPrepNowAsync();
|
|
Task RefineTaskAsync(string taskId);
|
|
|
|
event Action<string>? RefineStartedEvent;
|
|
event Action<string, bool, string?>? RefineFinishedEvent;
|
|
Task ClearMyDayAsync();
|
|
Task<AppSettingsDto?> GetAppSettingsAsync();
|
|
Task UpdateAppSettingsAsync(AppSettingsDto dto);
|
|
Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day);
|
|
Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text);
|
|
Task UpdateDailyNoteAsync(string id, string text);
|
|
Task DeleteDailyNoteAsync(string id);
|
|
Task<string> GetLastPrepLogAsync();
|
|
Task<IReadOnlyList<WorkerLogEntry>> GetRecentLogsAsync();
|
|
|
|
Task<List<PrimeScheduleDto>> GetPrimeSchedulesAsync();
|
|
Task<PrimeScheduleDto?> UpsertPrimeScheduleAsync(PrimeScheduleDto dto);
|
|
Task DeletePrimeScheduleAsync(Guid id);
|
|
|
|
Task UpdateListAsync(UpdateListDto dto);
|
|
Task UpdateListConfigAsync(UpdateListConfigDto dto);
|
|
|
|
Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null);
|
|
Task<WorktreeResetDto?> ResetAllWorktreesAsync();
|
|
Task<List<WorktreeOverviewDto>> GetWorktreesOverviewAsync(string? listId);
|
|
Task<(bool Ok, string? Error)> SetWorktreeStateAsync(string taskId, WorktreeState newState);
|
|
Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId);
|
|
|
|
Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync();
|
|
Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input);
|
|
Task SetOnlineInboxAuthAsync(string refreshToken);
|
|
Task ClearOnlineInboxAuthAsync();
|
|
|
|
/// <summary>Raised whenever the worker's usage poller ticks (success or failure).</summary>
|
|
event Action<UsageSnapshotDto>? UsageUpdatedEvent;
|
|
Task<UsageSnapshotDto?> GetUsageSnapshotAsync();
|
|
|
|
/// <summary>Forces an out-of-band usage poll on the worker and returns the fresh snapshot.</summary>
|
|
Task<UsageSnapshotDto?> RefreshUsageAsync();
|
|
Task<IReadOnlyList<ModelUsageRowDto>> GetModelUsageAsync(DateOnly from, DateOnly to);
|
|
Task<IReadOnlyList<TaskUsageRowDto>> GetTaskUsageAsync(DateOnly from, DateOnly to);
|
|
}
|