Files
ClaudeDo/src/ClaudeDo.Ui/Services/WorkerClient.cs
T
mika kunsandClaude Sonnet 5.5 feb0afa33a fix(merge): pass the stash consent to the conflict resolver so it opens on a dirty target
The resolver restarts the merge via StartConflictMerge, which had no autostash.
After a stash-retry approve reported 'conflict', the restart hit the same dirty
target, got 'dirty_target' and the resolver silently never opened. Thread the
consent through the hub, client, coordinator and resolver; surface a refusal in
the footer instead of dropping it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 17:25:52 +02:00

834 lines
39 KiB
C#

using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Threading;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
namespace ClaudeDo.Ui.Services;
public sealed record WorkerLogEntry(string Message, WorkerLogLevel Level, DateTime TimestampUtc);
sealed class IndefiniteRetryPolicy : IRetryPolicy
{
private static readonly TimeSpan[] _delays =
[
TimeSpan.Zero,
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30),
];
public TimeSpan? NextRetryDelay(RetryContext retryContext) =>
_delays[Math.Min(retryContext.PreviousRetryCount, _delays.Length - 1)];
}
public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerClient
{
private readonly HubConnection _hub;
private CancellationTokenSource? _startCts;
private Task _retryLoopTask = Task.CompletedTask;
private readonly object _startLock = new();
[ObservableProperty]
private bool _isConnected;
[ObservableProperty]
private bool _isReconnecting;
public ObservableCollection<ActiveTaskDto> ActiveTasks { get; } = new();
public event Action<string, string, DateTime>? TaskStartedEvent;
public event Action<string, string, string, DateTime>? TaskFinishedEvent;
public event Action<string, string>? TaskMessageEvent;
public event Action<string>? TaskUpdatedEvent;
public event Action<string, string, string>? TaskQuestionAskedEvent;
public event Action<string, string>? TaskQuestionResolvedEvent;
public event Action<string, IReadOnlyList<string>, string>? HandoffRequestedEvent;
public event Action<string, string>? SessionHandoffRequestedEvent;
public event Action<string, IReadOnlyList<string>>? ListHandlerStartRequestedEvent;
public event Action<string, string>? HandlerUsageWarningEvent;
public event Action<string, DateTimeOffset>? HandlerHandoffDeferredEvent;
public event Action? ConnectionRestoredEvent;
public event Action<string>? WorktreeUpdatedEvent;
public event Action<string>? ListUpdatedEvent;
public event Action<WorkerLogEntry>? WorkerLogReceivedEvent;
public event Action? NotesUpdatedEvent;
public event Action? PrepStartedEvent;
public event Action<string>? PrepLineEvent;
public event Action<bool>? PrepFinishedEvent;
public event Action<string>? RefineStartedEvent;
public event Action<string, bool, string?>? RefineFinishedEvent;
public event Action<UsageSnapshotDto>? UsageUpdatedEvent;
public event Action<string, string, int, int>? OperationProgressEvent;
public event Action<RunningOperationDto>? OperationStartedEvent;
public event Action<string>? OperationEndedEvent;
public event Action<string, string>? PlanningMergeStartedEvent;
public event Action<string, string>? PlanningSubtaskMergedEvent;
public event Action<string, string, IReadOnlyList<string>, bool>? PlanningMergeConflictEvent;
public event Action<string, string?>? PlanningMergeAbortedEvent;
public event Action<string>? PlanningCompletedEvent;
public event Action<PrimeFiredEvent>? PrimeFired;
public string? LastApproveTarget { get; private set; }
public IReadOnlyList<ActiveTaskDto> GetActiveTasks() => ActiveTasks.ToList();
public WorkerClient(string signalRUrl)
{
_hub = new HubConnectionBuilder()
.WithUrl(signalRUrl)
.WithAutomaticReconnect(new IndefiniteRetryPolicy())
.AddJsonProtocol(options =>
{
options.PayloadSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
})
.Build();
_hub.Reconnected += async _ =>
{
Dispatcher.UIThread.Post(() => { IsConnected = true; IsReconnecting = false; });
await SeedActiveTasksAsync();
Dispatcher.UIThread.Post(() => ConnectionRestoredEvent?.Invoke());
};
_hub.Reconnecting += _ =>
{
Dispatcher.UIThread.Post(() => { IsConnected = false; IsReconnecting = true; });
return Task.CompletedTask;
};
_hub.Closed += _ =>
{
Dispatcher.UIThread.Post(() =>
{
IsConnected = false;
IsReconnecting = false;
ActiveTasks.Clear();
});
return Task.CompletedTask;
};
_hub.On<string, string, DateTime>("TaskStarted", (slot, taskId, startedAt) =>
{
Dispatcher.UIThread.Post(() =>
{
ActiveTasks.Add(new ActiveTaskDto(slot, taskId, startedAt));
TaskStartedEvent?.Invoke(slot, taskId, startedAt);
});
});
_hub.On<string, string, string, DateTime>("TaskFinished", (slot, taskId, status, finishedAt) =>
{
Dispatcher.UIThread.Post(() =>
{
var existing = ActiveTasks.FirstOrDefault(t => t.TaskId == taskId);
if (existing is not null)
ActiveTasks.Remove(existing);
TaskFinishedEvent?.Invoke(slot, taskId, status, finishedAt);
});
});
_hub.On<string, string>("TaskMessage", (taskId, line) =>
{
Dispatcher.UIThread.Post(() => TaskMessageEvent?.Invoke(taskId, line));
});
_hub.On<string>("TaskUpdated", taskId =>
{
Dispatcher.UIThread.Post(() => TaskUpdatedEvent?.Invoke(taskId));
});
_hub.On<string, string, string>("TaskQuestionAsked", (taskId, questionId, question) =>
{
Dispatcher.UIThread.Post(() => TaskQuestionAskedEvent?.Invoke(taskId, questionId, question));
});
_hub.On<string, string>("TaskQuestionResolved", (taskId, questionId) =>
{
Dispatcher.UIThread.Post(() => TaskQuestionResolvedEvent?.Invoke(taskId, questionId));
});
_hub.On<string, IReadOnlyList<string>, string>("HandoffRequested", (taskId, survivingTaskIds, nextPhase) =>
{
Dispatcher.UIThread.Post(() => HandoffRequestedEvent?.Invoke(taskId, survivingTaskIds, nextPhase));
});
_hub.On<string, string>("SessionHandoffRequested", (paneId, briefPath) =>
{
Dispatcher.UIThread.Post(() => SessionHandoffRequestedEvent?.Invoke(paneId, briefPath));
});
_hub.On<string, IReadOnlyList<string>>("ListHandlerStartRequested", (listId, taskIds) =>
{
Dispatcher.UIThread.Post(() => ListHandlerStartRequestedEvent?.Invoke(listId, taskIds));
});
_hub.On<string, string>("HandlerUsageWarning", (taskId, message) =>
{
Dispatcher.UIThread.Post(() => HandlerUsageWarningEvent?.Invoke(taskId, message));
});
_hub.On<string, DateTimeOffset>("HandlerHandoffDeferred", (taskId, dueAt) =>
{
Dispatcher.UIThread.Post(() => HandlerHandoffDeferredEvent?.Invoke(taskId, dueAt));
});
_hub.On<string>("WorktreeUpdated", taskId =>
{
Dispatcher.UIThread.Post(() => WorktreeUpdatedEvent?.Invoke(taskId));
});
_hub.On<string>("ListUpdated", listId =>
{
Dispatcher.UIThread.Post(() => ListUpdatedEvent?.Invoke(listId));
});
_hub.On<string, WorkerLogLevel, DateTime>("WorkerLog", (message, level, timestampUtc) =>
{
Dispatcher.UIThread.Post(() =>
WorkerLogReceivedEvent?.Invoke(new WorkerLogEntry(message, level, timestampUtc)));
});
_hub.On<string, string, int, int>("OperationProgress", (opKey, phase, current, total) =>
{
Dispatcher.UIThread.Post(() => OperationProgressEvent?.Invoke(opKey, phase, current, total));
});
_hub.On<RunningOperationDto>("OperationStarted", op =>
{
Dispatcher.UIThread.Post(() => OperationStartedEvent?.Invoke(op));
});
_hub.On<string>("OperationEnded", id =>
{
Dispatcher.UIThread.Post(() => OperationEndedEvent?.Invoke(id));
});
_hub.On<string, string>("PlanningMergeStarted", (planningTaskId, targetBranch) =>
{
Dispatcher.UIThread.Post(() => PlanningMergeStartedEvent?.Invoke(planningTaskId, targetBranch));
});
_hub.On<string, string>("PlanningSubtaskMerged", (planningTaskId, subtaskId) =>
{
Dispatcher.UIThread.Post(() => PlanningSubtaskMergedEvent?.Invoke(planningTaskId, subtaskId));
});
_hub.On<string, string, IReadOnlyList<string>, bool>("PlanningMergeConflict", (planningTaskId, subtaskId, conflictedFiles, externallyDriven) =>
{
Dispatcher.UIThread.Post(() => PlanningMergeConflictEvent?.Invoke(planningTaskId, subtaskId, conflictedFiles, externallyDriven));
});
_hub.On<string, string?>("PlanningMergeAborted", (planningTaskId, reason) =>
{
Dispatcher.UIThread.Post(() => PlanningMergeAbortedEvent?.Invoke(planningTaskId, reason));
});
_hub.On<string>("PlanningCompleted", planningTaskId =>
{
Dispatcher.UIThread.Post(() => PlanningCompletedEvent?.Invoke(planningTaskId));
});
_hub.On<Guid, bool, string, DateTimeOffset>("PrimeFired", (id, ok, msg, when) =>
{
Dispatcher.UIThread.Post(() => PrimeFired?.Invoke(new PrimeFiredEvent(id, ok, msg, when)));
});
_hub.On("NotesUpdated", () => Dispatcher.UIThread.Post(() => NotesUpdatedEvent?.Invoke()));
_hub.On("PrepStarted", () => Dispatcher.UIThread.Post(() => PrepStartedEvent?.Invoke()));
_hub.On<string>("PrepLine", line => Dispatcher.UIThread.Post(() => PrepLineEvent?.Invoke(line)));
_hub.On<bool>("PrepFinished", ok => Dispatcher.UIThread.Post(() => PrepFinishedEvent?.Invoke(ok)));
_hub.On<string>("RefineStarted", id =>
Dispatcher.UIThread.Post(() => RefineStartedEvent?.Invoke(id)));
_hub.On<string, bool, string?>("RefineFinished", (id, ok, err) =>
Dispatcher.UIThread.Post(() => RefineFinishedEvent?.Invoke(id, ok, err)));
_hub.On<UsageSnapshotDto>("UsageUpdated", snapshot =>
Dispatcher.UIThread.Post(() => UsageUpdatedEvent?.Invoke(snapshot)));
}
public Task StartAsync()
{
lock (_startLock)
{
if (!_retryLoopTask.IsCompleted)
return Task.CompletedTask;
var old = _startCts;
_startCts = new CancellationTokenSource();
old?.Cancel();
old?.Dispose();
_retryLoopTask = ConnectWithRetryAsync(_startCts.Token);
}
return Task.CompletedTask;
}
private async Task ConnectWithRetryAsync(CancellationToken ct)
{
var delays = new[] { 0, 2, 5, 10, 30 };
int attempt = 0;
Dispatcher.UIThread.Post(() => IsReconnecting = true);
while (!ct.IsCancellationRequested)
{
try
{
await _hub.StartAsync(ct);
Dispatcher.UIThread.Post(() => { IsConnected = true; IsReconnecting = false; });
await SeedActiveTasksAsync();
Dispatcher.UIThread.Post(() => ConnectionRestoredEvent?.Invoke());
return;
}
catch (OperationCanceledException)
{
return;
}
catch
{
var delay = delays[Math.Min(attempt++, delays.Length - 1)];
try { await Task.Delay(TimeSpan.FromSeconds(delay), ct); }
catch (OperationCanceledException) { return; }
}
}
}
public async Task StopAsync()
{
_startCts?.Cancel();
try { await _retryLoopTask; } catch (OperationCanceledException) { } catch { /* swallow */ }
try { await _hub.StopAsync(); } catch { /* swallow */ }
}
/// <summary>Invoke a hub method, returning default (null) when the worker is offline or errors.</summary>
private async Task<T?> TryInvokeAsync<T>(string method, params object?[] args)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var ok = false;
try
{
var result = await _hub.InvokeCoreAsync<T>(method, args);
ok = true;
return result;
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
Serilog.Log.Warning(ex, "Hub call {method_name} failed", method);
return default;
}
catch { return default; }
finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); }
}
/// <summary>Times a void hub invoke without changing its exception/cancellation behavior.</summary>
private async Task InvokeTimedAsync(string method, Func<Task> invoke)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var ok = false;
try
{
await invoke();
ok = true;
}
catch (Exception ex)
{
Serilog.Log.Warning(ex, "Hub call {method_name} failed", method);
throw;
}
finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); }
}
/// <summary>Times a hub invoke that returns a value, without changing its exception/cancellation behavior.</summary>
private async Task<T> InvokeTimedAsync<T>(string method, Func<Task<T>> invoke)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var ok = false;
try
{
var result = await invoke();
ok = true;
return result;
}
catch (Exception ex)
{
Serilog.Log.Warning(ex, "Hub call {method_name} failed", method);
throw;
}
finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); }
}
public async Task RunNowAsync(string taskId)
{
await InvokeTimedAsync("RunNow", () => _hub.InvokeAsync("RunNow", taskId));
}
public async Task ContinueTaskAsync(string taskId, string followUpPrompt)
{
await InvokeTimedAsync("ContinueTask", () => _hub.InvokeAsync("ContinueTask", taskId, followUpPrompt));
}
public async Task AnswerTaskQuestionAsync(string taskId, string questionId, string answer)
{
try { await InvokeTimedAsync<bool>("AnswerTaskQuestion", () => _hub.InvokeAsync<bool>("AnswerTaskQuestion", taskId, questionId, answer)); }
catch { /* offline or already resolved — the UI clears optimistically */ }
}
public Task<PendingQuestionDto?> GetPendingQuestionAsync(string taskId)
=> TryInvokeAsync<PendingQuestionDto>("GetPendingQuestion", taskId);
public async Task ResetTaskAsync(string taskId)
{
await InvokeTimedAsync("ResetTask", () => _hub.InvokeAsync("ResetTask", taskId));
}
public async Task<MergeResultDto> MergeTaskAsync(
string taskId, string targetBranch, bool removeWorktree, string commitMessage, bool autostash = false)
{
return await InvokeTimedAsync<MergeResultDto>("MergeTask", () => _hub.InvokeAsync<MergeResultDto>(
"MergeTask", taskId, targetBranch, removeWorktree, commitMessage, autostash));
}
public Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch, bool autostash = false)
=> InvokeTimedAsync<MergeResultDto>("StartConflictMerge", () => _hub.InvokeAsync<MergeResultDto>("StartConflictMerge", taskId, targetBranch, autostash));
public Task<MergeConflictDocumentsDto> GetMergeConflictDocumentsAsync(string taskId)
=> InvokeTimedAsync<MergeConflictDocumentsDto>("GetMergeConflictDocuments", () => _hub.InvokeAsync<MergeConflictDocumentsDto>("GetMergeConflictDocuments", taskId));
public Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent)
=> InvokeTimedAsync("WriteConflictResolution", () => _hub.InvokeAsync("WriteConflictResolution", taskId, path, resolvedContent));
public Task<MergeResultDto> ContinueConflictMergeAsync(string taskId)
=> InvokeTimedAsync<MergeResultDto>("ContinueConflictMerge", () => _hub.InvokeAsync<MergeResultDto>("ContinueConflictMerge", taskId));
public Task AbortConflictMergeAsync(string taskId)
=> InvokeTimedAsync("AbortConflictMerge", () => _hub.InvokeAsync("AbortConflictMerge", taskId));
public Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId)
=> TryInvokeAsync<MergeTargetsDto>("GetMergeTargets", taskId);
public async Task CancelTaskAsync(string taskId)
{
await InvokeTimedAsync("CancelTask", () => _hub.InvokeAsync("CancelTask", taskId));
}
public async Task<(bool Ok, string? Error)> DeleteTaskAsync(string taskId)
{
try
{
await InvokeTimedAsync("DeleteTask", () => _hub.InvokeAsync("DeleteTask", taskId, CancellationToken.None));
return (true, null);
}
catch (HubException ex)
{
return (false, ex.Message);
}
catch (InvalidOperationException)
{
// Hub connection is not active (worker offline / not yet connected).
return (false, "Worker is offline. Reconnect and try again.");
}
}
public async Task WakeQueueAsync()
{
await InvokeTimedAsync("WakeQueue", () => _hub.InvokeAsync("WakeQueue"));
}
public async Task<List<AgentInfo>> GetAgentsAsync()
=> await TryInvokeAsync<List<AgentInfo>>("GetAgents") ?? [];
public async Task RefreshAgentsAsync()
{
await InvokeTimedAsync("RefreshAgents", () => _hub.InvokeAsync("RefreshAgents"));
}
public async Task<SeedResultDto?> RestoreDefaultAgentsAsync()
=> await InvokeTimedAsync<SeedResultDto>("RestoreDefaultAgents", () => _hub.InvokeAsync<SeedResultDto>("RestoreDefaultAgents"));
private async Task SeedActiveTasksAsync()
{
try
{
var active = await InvokeTimedAsync<List<ActiveTaskDto>>("GetActive", () => _hub.InvokeAsync<List<ActiveTaskDto>>("GetActive"));
Dispatcher.UIThread.Post(() =>
{
ActiveTasks.Clear();
foreach (var a in active)
ActiveTasks.Add(new ActiveTaskDto(a.Slot, a.TaskId, a.StartedAt));
});
}
catch (HubException)
{
// Expected: worker doesn't support GetActive yet
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"SeedActiveTasksAsync failed: {ex}");
}
}
public async ValueTask DisposeAsync()
{
_startCts?.Cancel();
try { await _retryLoopTask; } catch (OperationCanceledException) { } catch { /* swallow */ }
await _hub.DisposeAsync();
}
public Task<AppSettingsDto?> GetAppSettingsAsync()
=> TryInvokeAsync<AppSettingsDto>("GetAppSettings");
public async Task UpdateAppSettingsAsync(AppSettingsDto dto)
{
await InvokeTimedAsync("UpdateAppSettings", () => _hub.InvokeAsync("UpdateAppSettings", dto));
}
public async Task<List<PrimeScheduleDto>> GetPrimeSchedulesAsync()
=> await TryInvokeAsync<List<PrimeScheduleDto>>("ListPrimeSchedules") ?? new List<PrimeScheduleDto>();
public async Task<PrimeScheduleDto?> UpsertPrimeScheduleAsync(PrimeScheduleDto dto)
=> await InvokeTimedAsync<PrimeScheduleDto>("UpsertPrimeSchedule", () => _hub.InvokeAsync<PrimeScheduleDto>("UpsertPrimeSchedule", dto));
public async Task DeletePrimeScheduleAsync(Guid id)
{
try { await InvokeTimedAsync("DeletePrimeSchedule", () => _hub.InvokeAsync("DeletePrimeSchedule", id)); }
catch { /* offline */ }
}
private static string IsoDay(DateOnly d) => d.ToString("yyyy-MM-dd");
public Task<string?> GetWeekReportAsync(DateOnly start, DateOnly end)
=> TryInvokeAsync<string>("GetWeekReport", IsoDay(start), IsoDay(end));
public Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end)
=> InvokeTimedAsync<string>("GenerateWeekReport", () => _hub.InvokeAsync<string>("GenerateWeekReport", IsoDay(start), IsoDay(end)));
public Task<bool> RunDailyPrepNowAsync()
=> InvokeTimedAsync<bool>("RunDailyPrepNow", () => _hub.InvokeAsync<bool>("RunDailyPrepNow"));
public Task<bool> RunTicketImportNowAsync()
=> InvokeTimedAsync<bool>("RunTicketImportNow", () => _hub.InvokeAsync<bool>("RunTicketImportNow"));
public Task RefineTaskAsync(string taskId) => InvokeTimedAsync("RefineTask", () => _hub.InvokeAsync("RefineTask", taskId));
public Task ClearMyDayAsync()
=> InvokeTimedAsync("ClearMyDay", () => _hub.InvokeAsync("ClearMyDay"));
public async Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day)
=> await TryInvokeAsync<List<DailyNoteDto>>("GetDailyNotes", IsoDay(day)) ?? new List<DailyNoteDto>();
public async Task<List<DailyNoteDto>> GetDailyNotesBetweenAsync(DateOnly start, DateOnly end)
=> await TryInvokeAsync<List<DailyNoteDto>>("GetDailyNotesBetween", IsoDay(start), IsoDay(end)) ?? new List<DailyNoteDto>();
public async Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text)
=> await InvokeTimedAsync<DailyNoteDto>("AddDailyNote", () => _hub.InvokeAsync<DailyNoteDto>("AddDailyNote", IsoDay(day), text));
public async Task UpdateDailyNoteAsync(string id, string text)
=> await InvokeTimedAsync("UpdateDailyNote", () => _hub.InvokeAsync("UpdateDailyNote", id, text));
public async Task DeleteDailyNoteAsync(string id)
=> await InvokeTimedAsync("DeleteDailyNote", () => _hub.InvokeAsync("DeleteDailyNote", id));
public async Task<string> GetLastPrepLogAsync()
=> await TryInvokeAsync<string>("GetLastPrepLog") ?? string.Empty;
public async Task<IReadOnlyList<RunningOperationDto>> GetActiveOperationsAsync()
=> await TryInvokeAsync<List<RunningOperationDto>>("GetActiveOperations") ?? new List<RunningOperationDto>();
public async Task<IReadOnlyList<WorkerLogEntry>> GetRecentLogsAsync()
=> await TryInvokeAsync<List<WorkerLogEntry>>("GetRecentLogs") ?? new List<WorkerLogEntry>();
public async Task UpdateListAsync(UpdateListDto dto)
{
await InvokeTimedAsync("UpdateList", () => _hub.InvokeAsync("UpdateList", dto));
}
public async Task UpdateListConfigAsync(UpdateListConfigDto dto)
{
await InvokeTimedAsync("UpdateListConfig", () => _hub.InvokeAsync("UpdateListConfig", dto));
}
public Task<ListConfigDto?> GetListConfigAsync(string listId)
=> TryInvokeAsync<ListConfigDto>("GetListConfig", listId);
public async Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto)
{
await InvokeTimedAsync("UpdateTaskAgentSettings", () => _hub.InvokeAsync("UpdateTaskAgentSettings", dto));
}
public async Task<List<string>> GetRepoImportFoldersAsync()
=> await InvokeTimedAsync<List<string>>("GetRepoImportFolders", () => _hub.InvokeAsync<List<string>>("GetRepoImportFolders"));
public Task SetRepoImportFoldersAsync(List<string> folders)
=> InvokeTimedAsync("SetRepoImportFolders", () => _hub.InvokeAsync("SetRepoImportFolders", folders));
public async Task<List<SshWorkspace>> GetSshWorkspacesAsync()
=> await InvokeTimedAsync<List<SshWorkspace>>("GetSshWorkspaces", () => _hub.InvokeAsync<List<SshWorkspace>>("GetSshWorkspaces"));
public Task SetSshWorkspacesAsync(List<SshWorkspace> workspaces)
=> InvokeTimedAsync("SetSshWorkspaces", () => _hub.InvokeAsync("SetSshWorkspaces", workspaces));
public async Task<List<SessionSkillDto>> GetSessionSkillsAsync()
=> await TryInvokeAsync<List<SessionSkillDto>>("GetSessionSkills") ?? [];
public Task<List<string>> InstallSessionSkillAsync(string url)
=> InvokeTimedAsync<List<string>>("InstallSessionSkill", () => _hub.InvokeAsync<List<string>>("InstallSessionSkill", url));
public Task UpdateSessionSkillAsync(string sourceUrl)
=> InvokeTimedAsync("UpdateSessionSkill", () => _hub.InvokeAsync("UpdateSessionSkill", sourceUrl));
public Task RemoveSessionSkillAsync(string sourceUrl)
=> InvokeTimedAsync("RemoveSessionSkill", () => _hub.InvokeAsync("RemoveSessionSkill", sourceUrl));
public async Task<BaseDirtyWarningDto?> SetTaskStatusAsync(string taskId, ClaudeDo.Data.Models.TaskStatus status)
{
var result = await InvokeTimedAsync<SetTaskStatusResultDto>("SetTaskStatus", () => _hub.InvokeAsync<SetTaskStatusResultDto>("SetTaskStatus", taskId, status.ToString()));
return result?.BaseDirty;
}
public Task SetTaskDoneAsync(string taskId)
=> InvokeTimedAsync("SetTaskDone", () => _hub.InvokeAsync("SetTaskDone", taskId));
public Task UnsetTaskDoneAsync(string taskId)
=> InvokeTimedAsync("UnsetTaskDone", () => _hub.InvokeAsync("UnsetTaskDone", taskId));
public Task DequeueTaskAsync(string taskId)
=> InvokeTimedAsync("DequeueTask", () => _hub.InvokeAsync("DequeueTask", taskId));
public async Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch, bool autostash = false)
{
LastApproveTarget = targetBranch;
return await InvokeTimedAsync<MergeResultDto>("ApproveReview", () => _hub.InvokeAsync<MergeResultDto>("ApproveReview", taskId, targetBranch, autostash));
}
public Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch)
=> TryInvokeAsync<MergePreviewDto>("PreviewMerge", taskId, targetBranch);
public async Task RejectReviewToQueueAsync(string taskId, string feedback)
{
await InvokeTimedAsync("RejectReviewToQueue", () => _hub.InvokeAsync("RejectReviewToQueue", taskId, feedback));
}
public async Task RejectReviewToIdleAsync(string taskId)
{
await InvokeTimedAsync("RejectReviewToIdle", () => _hub.InvokeAsync("RejectReviewToIdle", taskId));
}
public async Task CancelReviewAsync(string taskId)
{
await InvokeTimedAsync("CancelReview", () => _hub.InvokeAsync("CancelReview", taskId));
}
public async Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null)
=> await InvokeTimedAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", () => _hub.InvokeAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", listId));
public async Task<WorktreeResetDto?> ResetAllWorktreesAsync()
=> await InvokeTimedAsync<WorktreeResetDto>("ResetAllWorktrees", () => _hub.InvokeAsync<WorktreeResetDto>("ResetAllWorktrees"));
public async Task<List<WorktreeOverviewDto>> GetWorktreesOverviewAsync(string? listId)
=> await TryInvokeAsync<List<WorktreeOverviewDto>>("GetWorktreesOverview", listId)
?? new List<WorktreeOverviewDto>();
public async Task<(bool Ok, string? Error)> SetWorktreeStateAsync(string taskId, WorktreeState newState)
{
try
{
var ok = await InvokeTimedAsync<bool>("SetWorktreeState", () => _hub.InvokeAsync<bool>("SetWorktreeState", taskId, newState));
return (ok, null);
}
catch (HubException ex)
{
return (false, ex.Message);
}
catch (Exception)
{
return (false, "Worker offline.");
}
}
public async Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId)
=> await InvokeTimedAsync<ForceRemoveResultDto>("ForceRemoveWorktree", () => _hub.InvokeAsync<ForceRemoveResultDto>("ForceRemoveWorktree", taskId));
public async Task<PlanningSessionStartInfo> StartPlanningSessionAsync(string taskId, CancellationToken ct = default)
=> await InvokeTimedAsync<PlanningSessionStartInfo>("StartPlanningSessionAsync", () => _hub.InvokeAsync<PlanningSessionStartInfo>("StartPlanningSessionAsync", taskId, ct));
public async Task<PlanningSessionResumeInfo> ResumePlanningSessionAsync(string taskId, CancellationToken ct = default)
=> await InvokeTimedAsync<PlanningSessionResumeInfo>("ResumePlanningSessionAsync", () => _hub.InvokeAsync<PlanningSessionResumeInfo>("ResumePlanningSessionAsync", taskId, ct));
public async Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default)
=> await InvokeTimedAsync("SubmitTaskForReview", () => _hub.InvokeAsync("SubmitTaskForReview", taskId, ct));
public async Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
=> await InvokeTimedAsync<LaunchSpec>("GetInteractiveLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetInteractiveLaunchSpec", taskId, ct));
public async Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, string paneId, CancellationToken ct = default)
=> await InvokeTimedAsync<LaunchSpec>("GetAdHocLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetAdHocLaunchSpec", directory, paneId, ct));
public async Task<LaunchSpec> GetSshLaunchSpecAsync(string workspaceId, string paneId, CancellationToken ct = default)
=> await InvokeTimedAsync<LaunchSpec>("GetSshLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetSshLaunchSpec", workspaceId, paneId, ct));
public async Task<LaunchSpec> GetSessionHandoffLaunchSpecAsync(string paneId, string cwd, string briefPath, string? taskId, CancellationToken ct = default)
=> await InvokeTimedAsync<LaunchSpec>("GetSessionHandoffLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetSessionHandoffLaunchSpec", paneId, cwd, briefPath, taskId, ct));
public async Task<LaunchSpec> GetResumeSessionLaunchSpecAsync(string sessionId, string paneId, CancellationToken ct = default)
=> await InvokeTimedAsync<LaunchSpec>("GetResumeSessionLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetResumeSessionLaunchSpec", sessionId, paneId, ct));
public async Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, string? model = null, CancellationToken ct = default)
=> await InvokeTimedAsync<LaunchSpec>("GetMergeHelperLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetMergeHelperLaunchSpec", taskIds, listId, handlerTaskId, model, ct));
public async Task<string> CreateMergeHelperTaskAsync(
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
=> await InvokeTimedAsync<string>("CreateMergeHelperTask", () => _hub.InvokeAsync<string>("CreateMergeHelperTask", taskIds, listId, title, descriptionHeader, ct));
public async Task<LaunchSpec> GetMergeHelperHandoffLaunchSpecAsync(
string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase, CancellationToken ct = default)
=> await InvokeTimedAsync<LaunchSpec>("GetMergeHelperHandoffLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetMergeHelperHandoffLaunchSpec", taskId, survivingTaskIds, nextPhase, ct));
public Task ReportInteractiveSessionExitedAsync(string taskId, int exitCode, CancellationToken ct = default)
=> InvokeTimedAsync("ReportInteractiveSessionExited", () => _hub.InvokeAsync("ReportInteractiveSessionExited", taskId, exitCode, ct));
public async Task<IReadOnlyList<HandlerOpenTaskDto>> GetHandlerOpenTasksAsync(string handlerTaskId, CancellationToken ct = default)
=> await InvokeTimedAsync<List<HandlerOpenTaskDto>>("GetHandlerOpenTasks", () => _hub.InvokeAsync<List<HandlerOpenTaskDto>>("GetHandlerOpenTasks", handlerTaskId, ct));
public async Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
=> await InvokeTimedAsync<LaunchSpec>("GetPlanningStartLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetPlanningStartLaunchSpec", taskId, ct));
public async Task<LaunchSpec> GetPlanningResumeLaunchSpecAsync(string taskId, CancellationToken ct = default)
=> await InvokeTimedAsync<LaunchSpec>("GetPlanningResumeLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetPlanningResumeLaunchSpec", taskId, ct));
public async Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default)
=> await InvokeTimedAsync<DiscardPlanningOutcome>("DiscardPlanningSessionAsync", () => _hub.InvokeAsync<DiscardPlanningOutcome>("DiscardPlanningSessionAsync", taskId, dequeueQueuedChildren, ct));
public async Task<int> FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default)
=> await InvokeTimedAsync<int>("FinalizePlanningSessionAsync", () => _hub.InvokeAsync<int>("FinalizePlanningSessionAsync", taskId, queueAgentTasks, ct));
public async Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct = default)
=> await InvokeTimedAsync<int>("GetPendingDraftCountAsync", () => _hub.InvokeAsync<int>("GetPendingDraftCountAsync", taskId, ct));
public async Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId)
=> await TryInvokeAsync<List<SubtaskDiffDto>>("GetPlanningAggregate", planningTaskId) ?? [];
public async Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch)
=> await InvokeTimedAsync<CombinedDiffResultDto>("BuildPlanningIntegrationBranch", () => _hub.InvokeAsync<CombinedDiffResultDto>("BuildPlanningIntegrationBranch", planningTaskId, targetBranch));
public async Task ContinuePlanningMergeAsync(string planningTaskId)
{
await InvokeTimedAsync("ContinuePlanningMerge", () => _hub.InvokeAsync("ContinuePlanningMerge", planningTaskId));
}
public async Task AbortPlanningMergeAsync(string planningTaskId)
{
await InvokeTimedAsync("AbortPlanningMerge", () => _hub.InvokeAsync("AbortPlanningMerge", planningTaskId));
}
public async Task<IReadOnlyList<PlanningMergeConflictStateDto>> GetActiveExternalPlanningMergeConflictsAsync()
=> await TryInvokeAsync<List<PlanningMergeConflictStateDto>>("GetActiveExternalPlanningMergeConflicts")
?? [];
public async Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default)
{
await InvokeTimedAsync("QueuePlanningSubtasksAsync", () => _hub.InvokeAsync("QueuePlanningSubtasksAsync", parentTaskId, ct));
}
public Task<ClaudeBinDto?> GetClaudeBinAsync()
=> TryInvokeAsync<ClaudeBinDto>("GetClaudeBin");
public Task<ClaudeBinDto?> SetClaudeBinAsync(string? value)
=> TryInvokeAsync<ClaudeBinDto>("SetClaudeBin", value);
public Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync()
=> TryInvokeAsync<OnlineInboxStateDto>("GetOnlineInboxState");
public async Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input)
=> await InvokeTimedAsync("SetOnlineInboxConfig", () => _hub.InvokeAsync("SetOnlineInboxConfig", input));
public async Task SetOnlineInboxAuthAsync(string refreshToken)
=> await InvokeTimedAsync("SetOnlineInboxAuth", () => _hub.InvokeAsync("SetOnlineInboxAuth", refreshToken));
public async Task ClearOnlineInboxAuthAsync()
=> await InvokeTimedAsync("ClearOnlineInboxAuth", () => _hub.InvokeAsync("ClearOnlineInboxAuth"));
public Task<UsageSnapshotDto?> GetUsageSnapshotAsync()
=> TryInvokeAsync<UsageSnapshotDto>("GetUsageSnapshot");
public Task<UsageSnapshotDto?> RefreshUsageAsync()
=> TryInvokeAsync<UsageSnapshotDto>("RefreshUsage");
public async Task<IReadOnlyList<ModelUsageRowDto>> GetModelUsageAsync(DateOnly from, DateOnly to)
=> await TryInvokeAsync<List<ModelUsageRowDto>>("GetModelUsage", from, to) ?? [];
public async Task<IReadOnlyList<TaskUsageRowDto>> GetTaskUsageAsync(DateOnly from, DateOnly to)
=> await TryInvokeAsync<List<TaskUsageRowDto>>("GetTaskUsage", from, to) ?? [];
public Task<TokenTrackerStatusDto?> GetTokenTrackerStatusAsync()
=> TryInvokeAsync<TokenTrackerStatusDto>("GetTokenTrackerStatus");
public Task<TokenTrackerStatusDto?> RefreshTokenTrackerAsync()
=> TryInvokeAsync<TokenTrackerStatusDto>("RefreshTokenTracker");
public Task<TokenTrackerStatusDto?> InstallTokenTrackerAsync()
=> TryInvokeAsync<TokenTrackerStatusDto>("InstallTokenTracker");
public Task<TokenTrackerDashboardDto?> OpenTokenTrackerDashboardAsync()
=> TryInvokeAsync<TokenTrackerDashboardDto>("OpenTokenTrackerDashboard");
public Task<TicketSettingsDto?> GetTicketSettingsAsync()
=> TryInvokeAsync<TicketSettingsDto>("GetTicketSettings");
public Task SetTicketFallbackListAsync(string? listId)
=> InvokeTimedAsync("SetTicketFallbackList", () => _hub.InvokeAsync("SetTicketFallbackList", listId));
public Task SetTicketApiBaseUrlAsync(string? baseUrl)
=> InvokeTimedAsync("SetTicketApiBaseUrl", () => _hub.InvokeAsync("SetTicketApiBaseUrl", baseUrl));
public Task SetTicketTokenAsync(string token)
=> InvokeTimedAsync("SetTicketToken", () => _hub.InvokeAsync("SetTicketToken", token));
public Task ClearTicketTokenAsync()
=> InvokeTimedAsync("ClearTicketToken", () => _hub.InvokeAsync("ClearTicketToken"));
public Task<TicketConnectionDto?> TestTicketConnectionAsync()
=> TryInvokeAsync<TicketConnectionDto>("TestTicketConnection");
public Task<AzureDevOpsSettingsDto?> GetAzureDevOpsSettingsAsync()
=> TryInvokeAsync<AzureDevOpsSettingsDto>("GetAzureDevOpsSettings");
public Task SetAzureDevOpsTokenAsync(string token)
=> InvokeTimedAsync("SetAzureDevOpsToken", () => _hub.InvokeAsync("SetAzureDevOpsToken", token));
public Task ClearAzureDevOpsTokenAsync()
=> InvokeTimedAsync("ClearAzureDevOpsToken", () => _hub.InvokeAsync("ClearAzureDevOpsToken"));
public Task<AzureDevOpsConnectionDto?> TestAzureDevOpsConnectionAsync()
=> TryInvokeAsync<AzureDevOpsConnectionDto>("TestAzureDevOpsConnection");
public async Task<List<TicketProjectDto>> GetTicketProjectsAsync()
=> await TryInvokeAsync<List<TicketProjectDto>>("GetTicketProjects") ?? [];
public Task<TicketImportResultDto> ImportTicketsAsync(string listId)
=> InvokeTimedAsync<TicketImportResultDto>("ImportTickets", () => _hub.InvokeAsync<TicketImportResultDto>("ImportTickets", listId));
// IWorkerClient explicit implementations (drop typed return values)
async Task IWorkerClient.StartPlanningSessionAsync(string taskId, CancellationToken ct)
=> await StartPlanningSessionAsync(taskId, ct);
async Task IWorkerClient.ResumePlanningSessionAsync(string taskId, CancellationToken ct)
=> await ResumePlanningSessionAsync(taskId, ct);
async Task<DiscardPlanningOutcome> IWorkerClient.DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren, CancellationToken ct)
=> await DiscardPlanningSessionAsync(taskId, dequeueQueuedChildren, ct);
async Task IWorkerClient.FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks, CancellationToken ct)
=> await FinalizePlanningSessionAsync(taskId, queueAgentTasks, ct);
async Task<int> IWorkerClient.GetPendingDraftCountAsync(string taskId, CancellationToken ct)
=> await GetPendingDraftCountAsync(taskId, ct);
}