feat(worker,ui): Online Inbox config + auth hub plumbing (Phase 2)
Hub: GetOnlineInboxState / SetOnlineInboxConfig / SetOnlineInboxAuth / ClearOnlineInboxAuth. WorkerConfig.SaveOnlineInbox persists only the online_inbox section. OnlineTokenStore + config registered always so hub methods work when sync is disabled. IWorkerClient surface + all test fakes synced. RedirectUri config (default http://localhost:8765/callback). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -98,4 +98,9 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -504,6 +504,18 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
await _hub.InvokeAsync("QueuePlanningSubtasksAsync", parentTaskId, ct);
|
||||
}
|
||||
|
||||
public Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync()
|
||||
=> TryInvokeAsync<OnlineInboxStateDto>("GetOnlineInboxState");
|
||||
|
||||
public async Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input)
|
||||
=> await _hub.InvokeAsync("SetOnlineInboxConfig", input);
|
||||
|
||||
public async Task SetOnlineInboxAuthAsync(string refreshToken)
|
||||
=> await _hub.InvokeAsync("SetOnlineInboxAuth", refreshToken);
|
||||
|
||||
public async Task ClearOnlineInboxAuthAsync()
|
||||
=> await _hub.InvokeAsync("ClearOnlineInboxAuth");
|
||||
|
||||
// IWorkerClient explicit implementations (drop typed return values)
|
||||
async Task IWorkerClient.StartPlanningSessionAsync(string taskId, CancellationToken ct)
|
||||
=> await StartPlanningSessionAsync(taskId, ct);
|
||||
@@ -568,3 +580,21 @@ public sealed record WorktreeOverviewDto(
|
||||
bool PathExistsOnDisk);
|
||||
|
||||
public sealed record ForceRemoveResultDto(bool Removed, string? Reason);
|
||||
|
||||
public sealed record OnlineInboxStateDto(
|
||||
bool Enabled,
|
||||
string ApiBaseUrl,
|
||||
string Authority,
|
||||
string ClientId,
|
||||
string Scopes,
|
||||
string RedirectUri,
|
||||
bool SignedIn);
|
||||
|
||||
public sealed record OnlineInboxConfigInputDto(
|
||||
bool Enabled,
|
||||
string ApiBaseUrl,
|
||||
int PollIntervalSeconds,
|
||||
string Authority,
|
||||
string ClientId,
|
||||
string Scopes,
|
||||
string RedirectUri);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Worker.Online;
|
||||
@@ -74,9 +75,38 @@ public sealed class WorkerConfig
|
||||
return cfg;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persists ONLY the <c>online_inbox</c> section back to <paramref name="path"/>
|
||||
/// (defaults to <see cref="DefaultConfigPath"/>) without rewriting any other fields.
|
||||
/// Reads the existing JSON, replaces the <c>online_inbox</c> node, and writes back indented.
|
||||
/// </summary>
|
||||
public void SaveOnlineInbox(string? path = null)
|
||||
{
|
||||
path ??= DefaultConfigPath;
|
||||
|
||||
var root = File.Exists(path)
|
||||
? JsonNode.Parse(File.ReadAllText(path)) as JsonObject ?? new JsonObject()
|
||||
: new JsonObject();
|
||||
|
||||
root["online_inbox"] = JsonSerializer.SerializeToNode(OnlineInbox, InboxSerializerOpts);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
File.WriteAllText(path, root.ToJsonString(WriteOpts));
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||
{
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true,
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions InboxSerializerOpts = new()
|
||||
{
|
||||
WriteIndented = false,
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions WriteOpts = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.Agents;
|
||||
using ClaudeDo.Worker.Config;
|
||||
using ClaudeDo.Worker.Lifecycle;
|
||||
using ClaudeDo.Worker.Online;
|
||||
using ClaudeDo.Worker.Planning;
|
||||
using ClaudeDo.Worker.Prime;
|
||||
using ClaudeDo.Worker.Queue;
|
||||
@@ -65,6 +67,24 @@ public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? S
|
||||
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
||||
public record SeedResultDto(int Copied, int Skipped);
|
||||
|
||||
public record OnlineInboxStateDto(
|
||||
bool Enabled,
|
||||
string ApiBaseUrl,
|
||||
string Authority,
|
||||
string ClientId,
|
||||
string Scopes,
|
||||
string RedirectUri,
|
||||
bool SignedIn);
|
||||
|
||||
public record OnlineInboxConfigInput(
|
||||
bool Enabled,
|
||||
string ApiBaseUrl,
|
||||
int PollIntervalSeconds,
|
||||
string Authority,
|
||||
string ClientId,
|
||||
string Scopes,
|
||||
string RedirectUri);
|
||||
|
||||
public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
{
|
||||
private static readonly string Version =
|
||||
@@ -89,6 +109,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
private readonly ITaskStateService _state;
|
||||
private readonly IWeekReportService _report;
|
||||
private readonly IRefineRunner _refineRunner;
|
||||
private readonly WorkerConfig _cfg;
|
||||
private readonly OnlineInboxConfig _onlineInboxConfig;
|
||||
private readonly OnlineTokenStore _onlineTokenStore;
|
||||
|
||||
public WorkerHub(
|
||||
QueueService queue,
|
||||
@@ -109,7 +132,10 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
IPrimeRunner primeRunner,
|
||||
ITaskStateService state,
|
||||
IWeekReportService report,
|
||||
IRefineRunner refineRunner)
|
||||
IRefineRunner refineRunner,
|
||||
WorkerConfig cfg,
|
||||
OnlineInboxConfig onlineInboxConfig,
|
||||
OnlineTokenStore onlineTokenStore)
|
||||
{
|
||||
_queue = queue;
|
||||
_waker = waker;
|
||||
@@ -130,6 +156,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
_state = state;
|
||||
_report = report;
|
||||
_refineRunner = refineRunner;
|
||||
_cfg = cfg;
|
||||
_onlineInboxConfig = onlineInboxConfig;
|
||||
_onlineTokenStore = onlineTokenStore;
|
||||
}
|
||||
|
||||
// Maps the two exceptions service methods throw into client-facing HubExceptions:
|
||||
@@ -684,4 +713,41 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
|
||||
return ids.Count;
|
||||
}
|
||||
|
||||
#pragma warning disable CA1416 // ClaudeDo.Worker is Windows-only; DPAPI calls are safe here.
|
||||
public OnlineInboxStateDto GetOnlineInboxState()
|
||||
{
|
||||
var signedIn = _onlineTokenStore.Read() is not null;
|
||||
return new OnlineInboxStateDto(
|
||||
_onlineInboxConfig.Enabled,
|
||||
_onlineInboxConfig.ApiBaseUrl,
|
||||
_onlineInboxConfig.Zitadel.Authority,
|
||||
_onlineInboxConfig.Zitadel.ClientId,
|
||||
_onlineInboxConfig.Zitadel.Scopes,
|
||||
_onlineInboxConfig.RedirectUri,
|
||||
signedIn);
|
||||
}
|
||||
|
||||
public void SetOnlineInboxConfig(OnlineInboxConfigInput input)
|
||||
{
|
||||
_onlineInboxConfig.Enabled = input.Enabled;
|
||||
_onlineInboxConfig.ApiBaseUrl = input.ApiBaseUrl ?? "";
|
||||
_onlineInboxConfig.PollIntervalSeconds = input.PollIntervalSeconds;
|
||||
_onlineInboxConfig.RedirectUri = input.RedirectUri ?? "http://localhost:8765/callback";
|
||||
_onlineInboxConfig.Zitadel.Authority = input.Authority ?? "";
|
||||
_onlineInboxConfig.Zitadel.ClientId = input.ClientId ?? "";
|
||||
_onlineInboxConfig.Zitadel.Scopes = input.Scopes ?? "openid offline_access";
|
||||
_cfg.SaveOnlineInbox();
|
||||
}
|
||||
|
||||
public void SetOnlineInboxAuth(string refreshToken)
|
||||
{
|
||||
_onlineTokenStore.Save(refreshToken);
|
||||
}
|
||||
|
||||
public void ClearOnlineInboxAuth()
|
||||
{
|
||||
_onlineTokenStore.Clear();
|
||||
}
|
||||
#pragma warning restore CA1416
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ public sealed class OnlineInboxConfig
|
||||
[JsonPropertyName("poll_interval_seconds")]
|
||||
public int PollIntervalSeconds { get; set; } = 60;
|
||||
|
||||
[JsonPropertyName("redirect_uri")]
|
||||
public string RedirectUri { get; set; } = "http://localhost:8765/callback";
|
||||
|
||||
[JsonPropertyName("zitadel")]
|
||||
public ZitadelClientConfig Zitadel { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -151,14 +151,19 @@ builder.Services.AddMcpServer()
|
||||
.WithTools<PlanningMcpService>()
|
||||
.WithTools<TaskRunMcpService>();
|
||||
|
||||
// Online Inbox — registered only when enabled.
|
||||
// OnlineInboxConfig and OnlineTokenStore are always registered so hub methods work
|
||||
// even when sync is disabled. The sync stack (api client, auth, hosted service) is
|
||||
// only registered when enabled.
|
||||
builder.Services.AddSingleton(cfg.OnlineInbox);
|
||||
#pragma warning disable CA1416 // ClaudeDo.Worker is Windows-only; DPAPI is fine here.
|
||||
builder.Services.AddSingleton<OnlineTokenStore>();
|
||||
#pragma warning restore CA1416
|
||||
|
||||
if (cfg.OnlineInbox.Enabled)
|
||||
{
|
||||
OnlineInboxApiClient.ValidateBaseUrl(cfg.OnlineInbox.ApiBaseUrl);
|
||||
builder.Services.AddSingleton(cfg.OnlineInbox);
|
||||
builder.Services.AddHttpClient();
|
||||
#pragma warning disable CA1416 // ClaudeDo.Worker is Windows-only; DPAPI is fine here.
|
||||
builder.Services.AddSingleton<OnlineTokenStore>();
|
||||
#pragma warning disable CA1416
|
||||
builder.Services.AddSingleton<IOnlineAuthProvider, ZitadelAuthProvider>();
|
||||
#pragma warning restore CA1416
|
||||
builder.Services.AddHttpClient<IOnlineInboxApi, OnlineInboxApiClient>(client =>
|
||||
|
||||
Reference in New Issue
Block a user