Files

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);