using System.ComponentModel; using System.Text.Json; using NucleicApp.Services; using NucleicProtocol.Interop; namespace NucleicApp.ViewModels; /// Presentation and intent surface for one session detail screen. Transcript rows remain owned /// by the shared Swift projector; this model forwards only that session's splice diffs. public sealed class SessionDetailViewModel : ViewModelBase, IDisposable { private readonly RendererStore store; private readonly HostConnection connection; private ProjectionDiff? lastProjection; private bool active; private bool disposed; public SessionDetailViewModel( string sessionId, RendererStore store, HostConnection connection) { SessionId = sessionId; this.store = store; this.connection = connection; store.PropertyChanged += StoreChanged; store.ProjectionChanged += ProjectionChanged; } public string SessionId { get; } public SessionSummary? Session => store.Sessions.FirstOrDefault(item => item.Id == SessionId); public IReadOnlyList PendingApprovals => store.PendingApprovals .Where(item => item.SessionId == SessionId).ToArray(); public ProjectionDiff? LastProjection { get => lastProjection; private set => Set(ref lastProjection, value); } public ProtocolResult Activate(ulong? sinceSequence = null) { ObjectDisposedException.ThrowIf(disposed, this); if (active) return ProtocolResult.Ok; var result = connection.SendIntent(ClientIntents.Subscribe(SessionId, sinceSequence)); if (result == ProtocolResult.Ok) active = true; return result; } public ProtocolResult SendText(string text) { ObjectDisposedException.ThrowIf(disposed, this); if (string.IsNullOrWhiteSpace(text)) return ProtocolResult.InvalidArgument; return connection.SendIntent(ClientIntents.SendInput(SessionId, text)); } public ProtocolResult Allow(ApprovalRequest approval, JsonElement? updatedInput = null) => Respond(approval, ClientIntents.ApprovalAllow(approval.Id, updatedInput)); public ProtocolResult Deny(ApprovalRequest approval, string? reason = null) => Respond(approval, ClientIntents.ApprovalDeny(approval.Id, reason)); public ProtocolResult AllowAlways(ApprovalRequest approval, ApprovalAlwaysScope scope) => Respond(approval, ClientIntents.ApprovalAllowAlways(approval.Id, scope)); public ProtocolResult CancelRun(ApprovalRequest approval) => Respond(approval, ClientIntents.ApprovalCancelRun(approval.Id)); private ProtocolResult Respond(ApprovalRequest approval, string intent) { ObjectDisposedException.ThrowIf(disposed, this); if (approval.SessionId != SessionId) return ProtocolResult.InvalidArgument; return connection.SendIntent(intent); } private void StoreChanged(object? sender, PropertyChangedEventArgs args) { if (args.PropertyName == nameof(RendererStore.Sessions)) Raise(nameof(Session)); else if (args.PropertyName == nameof(RendererStore.PendingApprovals)) Raise(nameof(PendingApprovals)); } private void ProjectionChanged(ProjectionDiff diff) { if (diff.Root.TryGetProperty("sessionID", out var session) && session.ValueKind == JsonValueKind.String && session.GetString() == SessionId) { LastProjection = diff; } } public void Dispose() { if (disposed) return; disposed = true; store.PropertyChanged -= StoreChanged; store.ProjectionChanged -= ProjectionChanged; if (active) _ = connection.SendIntent(ClientIntents.Unsubscribe(SessionId)); active = false; } }