feat(hub): Ticket-Settings, Projektliste und Import ueber den Hub
This commit is contained in:
@@ -89,6 +89,8 @@ public sealed class ListRepository
|
||||
existing.SessionSkills = config.SessionSkills;
|
||||
existing.VerifyCommand = config.VerifyCommand;
|
||||
existing.SerializeOnFileOverlap = config.SerializeOnFileOverlap;
|
||||
existing.PermissionMode = config.PermissionMode;
|
||||
existing.TicketProjectId = config.TicketProjectId;
|
||||
}
|
||||
await _context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
@@ -94,14 +94,15 @@ public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string?
|
||||
|
||||
public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType, bool IsManual = false, bool FindingsTracked = false);
|
||||
|
||||
// SerializeOnFileOverlap is tri-state on purpose: null = leave the stored flag alone. A caller that
|
||||
// doesn't own the field (anything but the list-settings modal) must not be able to clear it by
|
||||
// omission — SetConfigAsync copies the entity verbatim.
|
||||
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, bool? SerializeOnFileOverlap = null, string? PermissionMode = null);
|
||||
// SerializeOnFileOverlap and TicketProjectId are tri-state on purpose: null = leave the stored
|
||||
// value alone. A caller that doesn't own the field (anything but the list-settings modal) must not
|
||||
// be able to clear it by omission — SetConfigAsync copies the entity verbatim. TicketProjectId's
|
||||
// own scale: null = keep stored, <= 0 = clear the link, > 0 = set it.
|
||||
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, bool? SerializeOnFileOverlap = null, string? PermissionMode = null, int? TicketProjectId = null);
|
||||
|
||||
public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? PermissionMode = null);
|
||||
|
||||
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, bool SerializeOnFileOverlap = false, string? PermissionMode = null);
|
||||
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, bool SerializeOnFileOverlap = false, string? PermissionMode = null, int? TicketProjectId = null);
|
||||
|
||||
public record SeedResultDto(int Copied, int Skipped);
|
||||
|
||||
@@ -206,3 +207,8 @@ public sealed record LaunchSpec(
|
||||
string Exe,
|
||||
IReadOnlyList<string> Args,
|
||||
IReadOnlyDictionary<string, string> Env);
|
||||
|
||||
public record TicketSettingsDto(string? ApiBaseUrl, bool TokenSet);
|
||||
public record TicketConnectionDto(bool Ok, string? UserName, IReadOnlyList<string> Scopes, string? Error);
|
||||
public record TicketProjectDto(int Id, string Title, string DepartmentName);
|
||||
public record TicketImportResultDto(int Examined, int Created);
|
||||
|
||||
@@ -217,4 +217,15 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
/// <summary>Starts TokenTracker's local dashboard on the worker and returns its URL; opening
|
||||
/// the browser is the caller's job.</summary>
|
||||
Task<TokenTrackerDashboardDto?> OpenTokenTrackerDashboardAsync();
|
||||
|
||||
Task<TicketSettingsDto?> GetTicketSettingsAsync();
|
||||
Task SetTicketApiBaseUrlAsync(string? baseUrl);
|
||||
Task SetTicketTokenAsync(string token);
|
||||
Task ClearTicketTokenAsync();
|
||||
Task<TicketConnectionDto?> TestTicketConnectionAsync();
|
||||
Task<List<TicketProjectDto>> GetTicketProjectsAsync();
|
||||
/// <summary>Imports open tickets assigned to the token owner into the given list. Throws
|
||||
/// HubException (with a user-readable message) on failure so the caller can surface it via
|
||||
/// the footer error strip.</summary>
|
||||
Task<TicketImportResultDto> ImportTicketsAsync(string listId);
|
||||
}
|
||||
|
||||
@@ -706,6 +706,27 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public Task<TokenTrackerDashboardDto?> OpenTokenTrackerDashboardAsync()
|
||||
=> TryInvokeAsync<TokenTrackerDashboardDto>("OpenTokenTrackerDashboard");
|
||||
|
||||
public Task<TicketSettingsDto?> GetTicketSettingsAsync()
|
||||
=> TryInvokeAsync<TicketSettingsDto>("GetTicketSettings");
|
||||
|
||||
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 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);
|
||||
|
||||
+5
-2
@@ -106,10 +106,12 @@ public sealed class ConfigMcpTools
|
||||
// Fields this tool doesn't expose but that live on the same row. They must survive every
|
||||
// write here — ListRepository.SetConfigAsync copies the entity verbatim, so anything left
|
||||
// at its default would silently reset (a SerializeOnFileOverlap reset only shows up as
|
||||
// tasks no longer serializing, long after this write).
|
||||
// tasks no longer serializing, long after this write; a TicketProjectId reset silently
|
||||
// kills the list<->ticket-project link — the tool has no ticket parameter on purpose,
|
||||
// that link is UI-only).
|
||||
var hasUnrelatedSettings = existing is not null
|
||||
&& (existing.SessionSkills is not null || existing.SerializeOnFileOverlap
|
||||
|| existing.PermissionMode is not null);
|
||||
|| existing.PermissionMode is not null || existing.TicketProjectId is not null);
|
||||
|
||||
ListConfigDto? config;
|
||||
var allCleared = m is null && sp is null && ap is null && mt is null && vc is null;
|
||||
@@ -130,6 +132,7 @@ public sealed class ConfigMcpTools
|
||||
VerifyCommand = vc, SessionSkills = existing?.SessionSkills,
|
||||
SerializeOnFileOverlap = existing?.SerializeOnFileOverlap ?? false,
|
||||
PermissionMode = existing?.PermissionMode,
|
||||
TicketProjectId = existing?.TicketProjectId,
|
||||
}, cancellationToken);
|
||||
config = allCleared ? null : new ListConfigDto(m, sp, ap, mt, vc);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ using ClaudeDo.Worker.Report;
|
||||
using ClaudeDo.Worker.Report.Interfaces;
|
||||
using ClaudeDo.Worker.Skills;
|
||||
using ClaudeDo.Worker.State;
|
||||
using ClaudeDo.Worker.Tickets;
|
||||
using ClaudeDo.Worker.Usage;
|
||||
using ClaudeDo.Worker.Usage.Interfaces;
|
||||
using ClaudeDo.Worker.Usage.TokenTracker;
|
||||
@@ -81,6 +82,10 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
private readonly UsageMonitorService? _usageMonitor;
|
||||
private readonly TokenTrackerService? _tokenTracker;
|
||||
private readonly InteractiveReviewSubmissionService? _interactiveReviewSubmission;
|
||||
private readonly TicketSystemConfig _ticketConfig;
|
||||
private readonly TicketPatStore _ticketPat;
|
||||
private readonly TicketClientFactory _ticketClients;
|
||||
private readonly TicketImportService _ticketImport;
|
||||
|
||||
public WorkerHub(
|
||||
QueueService queue,
|
||||
@@ -115,7 +120,11 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
ITranscriptUsageReader? usageReader = null,
|
||||
UsageMonitorService? usageMonitor = null,
|
||||
InteractiveReviewSubmissionService? interactiveReviewSubmission = null,
|
||||
TokenTrackerService? tokenTracker = null)
|
||||
TokenTrackerService? tokenTracker = null,
|
||||
TicketSystemConfig ticketConfig = null!,
|
||||
TicketPatStore ticketPat = null!,
|
||||
TicketClientFactory ticketClients = null!,
|
||||
TicketImportService ticketImport = null!)
|
||||
{
|
||||
_queue = queue;
|
||||
_waker = waker;
|
||||
@@ -150,6 +159,10 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
_usageMonitor = usageMonitor;
|
||||
_interactiveReviewSubmission = interactiveReviewSubmission;
|
||||
_tokenTracker = tokenTracker;
|
||||
_ticketConfig = ticketConfig;
|
||||
_ticketPat = ticketPat;
|
||||
_ticketClients = ticketClients;
|
||||
_ticketImport = ticketImport;
|
||||
}
|
||||
|
||||
// Persistence boundary for the session_skills JSON-array columns (task/list/global).
|
||||
@@ -553,7 +566,17 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
var existing = await repo.GetConfigAsync(dto.ListId);
|
||||
var serializeOnFileOverlap = dto.SerializeOnFileOverlap ?? existing?.SerializeOnFileOverlap ?? false;
|
||||
|
||||
if (model is null && systemPrompt is null && agentPath is null && dto.MaxTurns is null && sessionSkills is null && verifyCommand is null && permissionMode is null && !serializeOnFileOverlap)
|
||||
// Gleiche Tri-State-Regel wie oben: null = gespeicherten Wert behalten, 0 = Verknüpfung
|
||||
// löschen. Ohne das würde jeder fremde Writer (set_list_config MCP-Tool, Agent-Settings)
|
||||
// die Ticket-Verknüpfung still kappen — SetConfigAsync kopiert verbatim.
|
||||
var ticketProjectId = dto.TicketProjectId switch
|
||||
{
|
||||
null => existing?.TicketProjectId,
|
||||
<= 0 => null,
|
||||
var id => id,
|
||||
};
|
||||
|
||||
if (model is null && systemPrompt is null && agentPath is null && dto.MaxTurns is null && sessionSkills is null && verifyCommand is null && permissionMode is null && !serializeOnFileOverlap && ticketProjectId is null)
|
||||
{
|
||||
await repo.DeleteConfigAsync(dto.ListId);
|
||||
}
|
||||
@@ -570,6 +593,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
VerifyCommand = verifyCommand,
|
||||
SerializeOnFileOverlap = serializeOnFileOverlap,
|
||||
PermissionMode = permissionMode,
|
||||
TicketProjectId = ticketProjectId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -593,7 +617,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
var repo = new ListRepository(ctx);
|
||||
var config = await repo.GetConfigAsync(listId);
|
||||
if (config is null) return null;
|
||||
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns, SkillsFromJson(config.SessionSkills), config.VerifyCommand, config.SerializeOnFileOverlap, config.PermissionMode);
|
||||
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns, SkillsFromJson(config.SessionSkills), config.VerifyCommand, config.SerializeOnFileOverlap, config.PermissionMode, config.TicketProjectId);
|
||||
}
|
||||
|
||||
public async Task<SetTaskStatusResultDto> SetTaskStatus(string taskId, string status)
|
||||
@@ -1249,4 +1273,59 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
.Take(100)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public TicketSettingsDto GetTicketSettings()
|
||||
=> new(_cfg.TicketApiBaseUrl, _ticketConfig.Token is not null);
|
||||
|
||||
public void SetTicketApiBaseUrl(string? baseUrl)
|
||||
{
|
||||
_cfg.TicketApiBaseUrl = string.IsNullOrWhiteSpace(baseUrl) ? null : baseUrl.Trim();
|
||||
_cfg.SaveTicketApiBaseUrl();
|
||||
}
|
||||
|
||||
public void SetTicketToken(string token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token)) _ticketPat.Store.Clear();
|
||||
else _ticketPat.Store.Save(token.Trim());
|
||||
}
|
||||
|
||||
public void ClearTicketToken() => _ticketPat.Store.Clear();
|
||||
|
||||
public async Task<TicketConnectionDto> TestTicketConnection()
|
||||
{
|
||||
var client = _ticketClients.Create();
|
||||
if (client is null)
|
||||
return new TicketConnectionDto(false, null, Array.Empty<string>(), "Base-URL oder Token fehlt.");
|
||||
try
|
||||
{
|
||||
var identity = await client.GetIdentityAsync(Context.ConnectionAborted);
|
||||
return new TicketConnectionDto(true, identity.UserName, identity.Scopes, null);
|
||||
}
|
||||
catch (TicketApiException ex)
|
||||
{
|
||||
return new TicketConnectionDto(false, null, Array.Empty<string>(), ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<TicketProjectDto>> GetTicketProjects()
|
||||
{
|
||||
var client = _ticketClients.Create();
|
||||
if (client is null) return new List<TicketProjectDto>();
|
||||
var projects = await client.GetProjectsAsync(Context.ConnectionAborted);
|
||||
return projects.Select(p => new TicketProjectDto(p.Id, p.Title, p.DepartmentName)).ToList();
|
||||
}
|
||||
|
||||
public async Task<TicketImportResultDto> ImportTickets(string listId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _ticketImport.ImportAsync(listId, Context.ConnectionAborted);
|
||||
await _broadcaster.ListUpdated(listId);
|
||||
return new TicketImportResultDto(result.Examined, result.Created);
|
||||
}
|
||||
catch (TicketApiException ex)
|
||||
{
|
||||
throw new HubException(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user