using System.Text.Json; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Threading; namespace AxCopilot.Services.Agent; /// /// Windows 알림 전송 도구. /// 장시간 작업 완료 알림, 사용자 확인 요청 등을 트레이 또는 앱 내 토스트로 표시합니다. /// public class NotifyTool : IAgentTool { public string Name => "notify_tool"; public string Description => "Send a notification to the user. Use this when: " + "a long-running task completes, an important result needs attention, " + "or you want to inform the user of something. " + "The notification appears as an in-app toast message."; public ToolParameterSchema Parameters => new() { Properties = new() { ["title"] = new() { Type = "string", Description = "Notification title (short, 1-2 words)", }, ["message"] = new() { Type = "string", Description = "Notification message (detail text)", }, ["level"] = new() { Type = "string", Description = "Notification level: info (default), success, warning, error", Enum = ["info", "success", "warning", "error"], }, }, Required = ["title", "message"], }; public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default) { var title = args.GetProperty("title").GetString() ?? "알림"; var message = args.GetProperty("message").GetString() ?? ""; var level = args.TryGetProperty("level", out var lv) ? lv.GetString() ?? "info" : "info"; try { // InvokeAsync로 변경 — Dispatcher.Invoke는 UI 스레드가 _convLock 대기 중일 때 데드락 발생 await Application.Current.Dispatcher.InvokeAsync(() => { ShowToast(title, message, level); }); return ToolResult.Ok($"✓ Notification sent: [{level}] {title}"); } catch (Exception ex) { return ToolResult.Fail($"알림 전송 실패: {ex.Message}"); } } private static void ShowToast(string title, string message, string level) { var mainWindow = Application.Current.MainWindow; if (mainWindow == null) return; var (iconChar, iconColor) = level switch { "success" => ("\uE73E", "#34D399"), "warning" => ("\uE7BA", "#F59E0B"), "error" => ("\uEA39", "#F87171"), _ => ("\uE946", "#4B5EFC"), // info }; var toast = new Border { Background = new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E)), CornerRadius = new CornerRadius(10), Padding = new Thickness(16, 12, 16, 12), Margin = new Thickness(0, 0, 20, 20), MinWidth = 280, MaxWidth = 400, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Bottom, Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 16, ShadowDepth = 4, Opacity = 0.4, Color = Colors.Black, }, }; var content = new StackPanel(); // 타이틀 행 var titleRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 4) }; titleRow.Children.Add(new TextBlock { Text = iconChar, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 14, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(iconColor)), Margin = new Thickness(0, 0, 8, 0), VerticalAlignment = VerticalAlignment.Center, }); titleRow.Children.Add(new TextBlock { Text = title, FontSize = 13, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White, VerticalAlignment = VerticalAlignment.Center, }); content.Children.Add(titleRow); // 메시지 if (!string.IsNullOrEmpty(message)) { content.Children.Add(new TextBlock { Text = message.Length > 200 ? message[..200] + "..." : message, FontSize = 12, Foreground = new SolidColorBrush(Color.FromRgb(0xAA, 0xAA, 0xCC)), TextWrapping = TextWrapping.Wrap, }); } toast.Child = content; // 기존 Grid/Panel에 추가 if (mainWindow.Content is Grid grid) { Grid.SetRowSpan(toast, grid.RowDefinitions.Count > 0 ? grid.RowDefinitions.Count : 1); Grid.SetColumnSpan(toast, grid.ColumnDefinitions.Count > 0 ? grid.ColumnDefinitions.Count : 1); grid.Children.Add(toast); // 5초 후 자동 제거 (페이드 아웃) var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) }; timer.Tick += (_, _) => { timer.Stop(); grid.Children.Remove(toast); }; timer.Start(); } } }