156 lines
6.2 KiB
C#
156 lines
6.2 KiB
C#
using System.ComponentModel;
|
|||
|
|
using System.Runtime.CompilerServices;
|
||
|
|
using System.Text.Json;
|
||
|
|
using NucleicProtocol.Interop;
|
||
|
|
|
||
|
|
namespace NucleicApp.Services;
|
||
|
|
|
||
|
|
/// Read-only projection of host authority (WINDOWS_PORT §7). Every mutation enters through a
|
||
|
|
/// HostMsg; user actions leave as ClientMsg intents through ``HostConnection``.
|
||
|
|
public sealed class RendererStore : INotifyPropertyChanged, IDisposable
|
||
|
|
{
|
||
|
|
private readonly Dictionary<string, SessionSummary> sessions = new(StringComparer.Ordinal);
|
||
|
|
private readonly Dictionary<string, ApprovalRequest> approvals = new(StringComparer.Ordinal);
|
||
|
|
private readonly ITranscriptProjector? projector;
|
||
|
|
|
||
|
|
private IReadOnlyList<SessionSummary> sessionSnapshot = [];
|
||
|
|
private IReadOnlyList<ApprovalRequest> approvalSnapshot = [];
|
||
|
|
private JsonElement? dashboard;
|
||
|
|
private JsonElement? settings;
|
||
|
|
private JsonElement? peers;
|
||
|
|
private JsonElement? intelligenceCatalog;
|
||
|
|
private string? lastError;
|
||
|
|
|
||
|
|
public RendererStore(ITranscriptProjector? projector = null) => this.projector = projector;
|
||
|
|
|
||
|
|
public IReadOnlyList<SessionSummary> Sessions => sessionSnapshot;
|
||
|
|
public IReadOnlyList<ApprovalRequest> PendingApprovals => approvalSnapshot;
|
||
|
|
public JsonElement? Dashboard => dashboard;
|
||
|
|
public JsonElement? Settings => settings;
|
||
|
|
public JsonElement? Peers => peers;
|
||
|
|
public JsonElement? IntelligenceCatalog => intelligenceCatalog;
|
||
|
|
public string? LastError => lastError;
|
||
|
|
|
||
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||
|
|
public event Action<ProjectionDiff>? ProjectionChanged;
|
||
|
|
public event Action<ProtocolMessage>? MessageApplied;
|
||
|
|
|
||
|
|
public void Apply(string hostMessageJson) => Apply(ProtocolMessage.Parse(hostMessageJson));
|
||
|
|
|
||
|
|
public void Apply(ProtocolMessage message)
|
||
|
|
{
|
||
|
|
var root = message.Root;
|
||
|
|
switch (message.Tag)
|
||
|
|
{
|
||
|
|
case "sessionList":
|
||
|
|
sessions.Clear();
|
||
|
|
if (root.TryGetProperty("input", out var list))
|
||
|
|
foreach (var item in list.EnumerateArray()) UpsertSession(item);
|
||
|
|
PublishSessions();
|
||
|
|
break;
|
||
|
|
case "sessionUpdated":
|
||
|
|
UpsertSession(root);
|
||
|
|
PublishSessions();
|
||
|
|
break;
|
||
|
|
case "snapshot":
|
||
|
|
if (root.TryGetProperty("summary", out var summary))
|
||
|
|
{
|
||
|
|
UpsertSession(summary);
|
||
|
|
PublishSessions();
|
||
|
|
}
|
||
|
|
if (root.TryGetProperty("pendingApprovals", out var pending))
|
||
|
|
{
|
||
|
|
foreach (var item in pending.EnumerateArray()) UpsertApproval(item);
|
||
|
|
PublishApprovals();
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case "approvalRequested":
|
||
|
|
UpsertApproval(root);
|
||
|
|
PublishApprovals();
|
||
|
|
break;
|
||
|
|
case "approvalResolved":
|
||
|
|
if (String(root, "id") is { } resolved) approvals.Remove(resolved);
|
||
|
|
PublishApprovals();
|
||
|
|
break;
|
||
|
|
case "dashboard":
|
||
|
|
dashboard = root.Clone();
|
||
|
|
Changed(nameof(Dashboard));
|
||
|
|
break;
|
||
|
|
case "settings":
|
||
|
|
settings = root.GetProperty("settings").Clone();
|
||
|
|
Changed(nameof(Settings));
|
||
|
|
break;
|
||
|
|
case "peerList":
|
||
|
|
peers = root.GetProperty("input").Clone();
|
||
|
|
Changed(nameof(Peers));
|
||
|
|
break;
|
||
|
|
case "intelligenceCatalog":
|
||
|
|
intelligenceCatalog = root.GetProperty("intelligenceCatalog").Clone();
|
||
|
|
Changed(nameof(IntelligenceCatalog));
|
||
|
|
break;
|
||
|
|
case "error":
|
||
|
|
lastError = String(root, "message") ?? "The host reported an error.";
|
||
|
|
Changed(nameof(LastError));
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (projector is not null)
|
||
|
|
{
|
||
|
|
var diff = projector.Apply(message.Json);
|
||
|
|
if (diff.Root.GetProperty("splices").GetArrayLength() > 0)
|
||
|
|
ProjectionChanged?.Invoke(diff);
|
||
|
|
}
|
||
|
|
MessageApplied?.Invoke(message);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void UpsertSession(JsonElement json)
|
||
|
|
{
|
||
|
|
var id = String(json, "sessionID") ?? throw new JsonException("session summary has no sessionID");
|
||
|
|
sessions[id] = new SessionSummary(
|
||
|
|
id, String(json, "title") ?? "Untitled", String(json, "projectID"),
|
||
|
|
String(json, "projectName"), String(json, "status") ?? "unknown",
|
||
|
|
Int(json, "pendingApprovalCount"), json.Clone());
|
||
|
|
}
|
||
|
|
|
||
|
|
private void UpsertApproval(JsonElement json)
|
||
|
|
{
|
||
|
|
var id = String(json, "id") ?? throw new JsonException("approval has no id");
|
||
|
|
approvals[id] = new ApprovalRequest(
|
||
|
|
id, String(json, "sessionID") ?? string.Empty, String(json, "title") ?? "Approval",
|
||
|
|
String(json, "toolName") ?? string.Empty, String(json, "risk") ?? string.Empty,
|
||
|
|
json.Clone());
|
||
|
|
}
|
||
|
|
|
||
|
|
private void PublishSessions()
|
||
|
|
{
|
||
|
|
sessionSnapshot = sessions.Values
|
||
|
|
.OrderByDescending(item => item.Raw.TryGetProperty("updatedAt", out var value)
|
||
|
|
? value.GetDouble() : 0)
|
||
|
|
.ToArray();
|
||
|
|
Changed(nameof(Sessions));
|
||
|
|
}
|
||
|
|
|
||
|
|
private void PublishApprovals()
|
||
|
|
{
|
||
|
|
approvalSnapshot = approvals.Values.ToArray();
|
||
|
|
Changed(nameof(PendingApprovals));
|
||
|
|
}
|
||
|
|
|
||
|
|
private static string? String(JsonElement root, string name) =>
|
||
|
|
root.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.String
|
||
|
|
? value.GetString() : null;
|
||
|
|
private static int Int(JsonElement root, string name) =>
|
||
|
|
root.TryGetProperty(name, out var value) && value.TryGetInt32(out var number) ? number : 0;
|
||
|
|
private void Changed([CallerMemberName] string? name = null) =>
|
||
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||
|
|
|
||
|
|
public void Dispose() => projector?.Dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
public sealed record SessionSummary(
|
||
|
|
string Id, string Title, string? ProjectId, string? ProjectName,
|
||
|
|
string Status, int PendingApprovalCount, JsonElement Raw);
|
||
|
|
|
||
|
|
public sealed record ApprovalRequest(
|
||
|
|
string Id, string SessionId, string Title, string ToolName, string Risk, JsonElement Raw);
|