Merge nucleic/warm-north-vole-pf37 into dev
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user