Some checks failed
Release Gate / gate (push) Has been cancelled
- claude-code 선택적 탐색 흐름을 참고해 Cowork/Code 시스템 프롬프트에서 folder_map 상시 선행 지시를 완화하고 glob/grep 기반 좁은 탐색을 우선하도록 조정함 - FolderMapTool 기본 depth를 2로, include_files 기본값을 false로 낮추고 MultiReadTool 최대 파일 수를 8개로 줄여 초기 과탐색 폭을 보수적으로 조정함 - AgentLoopExplorationPolicy partial을 추가해 탐색 범위 분류, broad-scan corrective hint, exploration_breadth 성능 로그를 연결함 - AgentLoopService에 탐색 범위 가이드 주입과 실행 중 탐색 폭 추적을 추가하고, 좁은 질문에서 반복적인 folder_map/대량 multi_read를 교정하도록 정리함 - DocxToHtmlConverter nullable 경고를 수정해 Release 빌드 경고 0 / 오류 0 기준을 다시 충족함 - README와 docs/DEVELOPMENT.md에 2026-04-09 10:36 (KST) 기준 개발 이력을 반영함
153 lines
5.6 KiB
C#
153 lines
5.6 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").SafeGetString() ?? "알림";
|
|
var message = args.GetProperty("message").SafeGetString() ?? "";
|
|
var level = args.SafeTryGetProperty("level", out var lv) ? lv.SafeGetString() ?? "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초 후 자동 제거 — DispatcherTimer 대신 애니메이션 Completed 사용하여 타이머 누적 방지
|
|
var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(300))
|
|
{
|
|
BeginTime = TimeSpan.FromSeconds(5),
|
|
};
|
|
fadeOut.Completed += (_, _) => grid.Children.Remove(toast);
|
|
toast.BeginAnimation(System.Windows.UIElement.OpacityProperty, fadeOut);
|
|
}
|
|
}
|
|
}
|