Files
AX-Copilot-Codex/src/AxCopilot/Services/Agent/NotifyTool.cs
lacvet 1b4a2bfb1c AX Agent 진행 시간·글로우 경로 정리 및 최근 로컬 변경 일괄 반영
- AX Agent 스트리밍 경과 시간을 공용 helper로 통일해 비정상적인 수천만 시간 표시를 방지함

- 채팅 입력창 글로우를 런처와 같은 표시/숨김 중심의 얇은 외곽 글로우로 정리하고 런처 글로우 설정은 일반 설정에 유지함

- README와 DEVELOPMENT 문서를 2026-04-08 12:02 (KST) 기준으로 갱신하고 Release 빌드 경고 0 / 오류 0을 확인함
2026-04-08 23:20:53 +09:00

154 lines
5.4 KiB
C#

using System.Text.Json;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
namespace AxCopilot.Services.Agent;
/// <summary>
/// Windows 알림 전송 도구.
/// 장시간 작업 완료 알림, 사용자 확인 요청 등을 트레이 또는 앱 내 토스트로 표시합니다.
/// </summary>
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<ToolResult> 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();
}
}
}