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 sessions = new(StringComparer.Ordinal); private readonly Dictionary approvals = new(StringComparer.Ordinal); private readonly ITranscriptProjector? projector; private IReadOnlyList sessionSnapshot = []; private IReadOnlyList 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 Sessions => sessionSnapshot; public IReadOnlyList 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? ProjectionChanged; public event Action? 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);