Files

99 lines
3.7 KiB
C#

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<ApprovalRequest> 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;
}
}