Public Access
Preserve recovery transcripts and add user-approved agent requests
PR and Push Build/Test / build-and-test (push) Successful in 12m0s
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.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user