Public Access
PR and Push Build/Test / build-and-test (push) Successful in 12m0s
Append successful offline recovery with a duplicate-warning marker and retain failed replay sources. Offer newly available microphones through confirmation notifications. Add project discovery metadata and blocking association approval, plus nonblocking workflow requests that start a separate settings-agent conversation. Include regression tests, OpenSpec changes, and verified local Release startup.
86 lines
3.5 KiB
C#
86 lines
3.5 KiB
C#
#if WINDOWS
|
|
using System.Collections.Concurrent;
|
|
using CommunityToolkit.WinUI.Notifications;
|
|
using MeetingAssistant.Recording;
|
|
|
|
namespace MeetingAssistant.Notifications;
|
|
|
|
public sealed class WindowsAgentApprovalPromptService(ILogger<WindowsAgentApprovalPromptService> logger)
|
|
: IAgentApprovalPromptService, IDisposable
|
|
{
|
|
private const string Source = "summary-agent-approval";
|
|
private readonly ConcurrentDictionary<string, TaskCompletionSource<bool>> pending = new();
|
|
private readonly object gate = new();
|
|
private bool registered;
|
|
private bool disposed;
|
|
|
|
public async Task<bool> ConfirmAsync(AgentApprovalPrompt prompt, CancellationToken cancellationToken)
|
|
{
|
|
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17763)) return false;
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
var id = Guid.NewGuid().ToString("N");
|
|
var response = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
try
|
|
{
|
|
lock (gate)
|
|
{
|
|
if (disposed) return false;
|
|
if (!registered)
|
|
{
|
|
ToastNotificationManagerCompat.OnActivated += OnActivated;
|
|
registered = true;
|
|
}
|
|
pending[id] = response;
|
|
new ToastContentBuilder()
|
|
.AddArgument("source", Source).AddArgument("requestId", id)
|
|
.SetToastScenario(ToastScenario.Reminder)
|
|
.SetToastDuration(ToastDuration.Long)
|
|
.AddText(prompt.Title)
|
|
.AddText(prompt.Message)
|
|
.AddButton(new ToastButton().SetContent("Yes")
|
|
.AddArgument("source", Source).AddArgument("requestId", id)
|
|
.AddArgument("response", "yes").SetBackgroundActivation())
|
|
.AddButton(new ToastButton().SetContent("No")
|
|
.AddArgument("source", Source).AddArgument("requestId", id)
|
|
.AddArgument("response", "no").SetBackgroundActivation())
|
|
.Show(toast =>
|
|
{
|
|
toast.Tag = id;
|
|
toast.Group = Source;
|
|
toast.ExpirationTime = DateTimeOffset.Now.Add(prompt.Timeout);
|
|
});
|
|
}
|
|
|
|
return await response.Task.WaitAsync(cancellationToken);
|
|
}
|
|
finally
|
|
{
|
|
pending.TryRemove(id, out _);
|
|
try { ToastNotificationManagerCompat.History.Remove(id, Source); }
|
|
catch (Exception exception) { logger.LogDebug(exception, "Could not remove approval notification {RequestId}", id); }
|
|
}
|
|
}
|
|
|
|
private void OnActivated(ToastNotificationActivatedEventArgsCompat args)
|
|
{
|
|
var arguments = NotificationActivationArguments.Parse(args.Argument);
|
|
if (arguments.GetValueOrDefault("source") != Source ||
|
|
!arguments.TryGetValue("requestId", out var id) ||
|
|
!pending.TryRemove(id, out var response)) return;
|
|
response.TrySetResult(arguments.GetValueOrDefault("response") == "yes");
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
lock (gate)
|
|
{
|
|
disposed = true;
|
|
if (registered) ToastNotificationManagerCompat.OnActivated -= OnActivated;
|
|
registered = false;
|
|
foreach (var response in pending.Values) response.TrySetResult(false);
|
|
pending.Clear();
|
|
}
|
|
}
|
|
}
|
|
#endif
|