using System; using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Media.Effects; namespace AxCopilot.Views; /// /// 도구 실행 승인을 위한 간결한 별도 다이얼로그 창. /// PlanViewerV2(전체 계획 뷰어)와 분리하여 도구 단위 승인에 사용합니다. /// internal sealed class ToolApprovalWindow : Window { private string? _result; private ToolApprovalWindow(string message, List options) { Width = 500; MinWidth = 400; MaxWidth = 600; SizeToContent = SizeToContent.Height; WindowStartupLocation = WindowStartupLocation.CenterScreen; ResizeMode = ResizeMode.NoResize; WindowStyle = WindowStyle.None; AllowsTransparency = true; Background = Brushes.Transparent; ShowInTaskbar = false; Topmost = true; var bg = Application.Current.TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E)); var primary = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondary = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var accent = Application.Current.TryFindResource("AccentColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); var border = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var itemBg = Application.Current.TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40)); var errorBrush = Application.Current.TryFindResource("ErrorColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)); var hoverBg = Application.Current.TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x20, 0xFF, 0xFF, 0xFF)); var root = new Border { Background = bg, BorderBrush = border, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(14), Padding = new Thickness(20, 16, 20, 16), Effect = new DropShadowEffect { BlurRadius = 20, ShadowDepth = 4, Opacity = 0.35, Color = Colors.Black, }, }; var stack = new StackPanel(); // Header var header = new Grid { Margin = new Thickness(0, 0, 0, 12) }; header.MouseLeftButtonDown += (_, _) => { try { DragMove(); } catch { } }; header.Children.Add(new StackPanel { Orientation = Orientation.Horizontal, Children = { new TextBlock { Text = "\uE946", // Shield icon FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 15, Foreground = accent, Margin = new Thickness(0, 0, 8, 0), VerticalAlignment = VerticalAlignment.Center, }, new TextBlock { Text = "실행 확인", FontSize = 13.5, FontWeight = FontWeights.SemiBold, Foreground = primary, VerticalAlignment = VerticalAlignment.Center, } } }); // Close button var close = new Border { Width = 26, Height = 26, CornerRadius = new CornerRadius(7), Background = Brushes.Transparent, Cursor = Cursors.Hand, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center, Child = new TextBlock { Text = "\uE8BB", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 10, Foreground = secondary, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }, }; close.MouseLeftButtonUp += (_, _) => { _result = "취소"; Close(); }; close.MouseEnter += (_, _) => close.Background = hoverBg; close.MouseLeave += (_, _) => close.Background = Brushes.Transparent; header.Children.Add(close); stack.Children.Add(header); // Message content var msgBorder = new Border { Background = itemBg, CornerRadius = new CornerRadius(12), Padding = new Thickness(14, 11, 14, 11), Margin = new Thickness(0, 0, 0, 14), }; var msgText = new TextBlock { Text = message, FontSize = 13, FontFamily = new FontFamily("Segoe UI"), Foreground = primary, TextWrapping = TextWrapping.Wrap, LineHeight = 20, }; msgBorder.Child = msgText; stack.Children.Add(msgBorder); // Buttons var btnPanel = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, }; foreach (var option in options) { var btn = CreateOptionButton(option, primary, secondary, accent, bg, errorBrush); btn.MouseLeftButtonUp += (_, _) => { _result = option; Close(); }; btnPanel.Children.Add(btn); } stack.Children.Add(btnPanel); root.Child = stack; Content = root; // Entrance animation root.Opacity = 0; root.RenderTransformOrigin = new Point(0.5, 0.5); root.RenderTransform = new ScaleTransform(0.96, 0.96); Loaded += (_, _) => { root.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(140))); var sx = new DoubleAnimation(0.96, 1, TimeSpan.FromMilliseconds(180)) { EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }, }; var sy = new DoubleAnimation(0.96, 1, TimeSpan.FromMilliseconds(180)) { EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }, }; ((ScaleTransform)root.RenderTransform).BeginAnimation(ScaleTransform.ScaleXProperty, sx); ((ScaleTransform)root.RenderTransform).BeginAnimation(ScaleTransform.ScaleYProperty, sy); }; // ESC to cancel KeyDown += (_, e) => { if (e.Key == Key.Escape) { _result = "취소"; Close(); } }; } private Border CreateOptionButton(string label, Brush primary, Brush secondary, Brush accent, Brush bg, Brush errorBrush) { Brush foreground, background, borderBrush; switch (label) { case "확인": case "승인": foreground = Brushes.White; background = accent; borderBrush = accent; break; case "취소": case "중단": foreground = errorBrush; background = Brushes.Transparent; borderBrush = errorBrush; break; default: foreground = primary; background = Brushes.Transparent; borderBrush = secondary; break; } var isDestructive = label == "취소" || label == "중단"; // Build inner content: optional left accent bar + label FrameworkElement child; if (isDestructive) { var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); var accentBar = new Border { Width = 4, CornerRadius = new CornerRadius(2), Background = errorBrush, Margin = new Thickness(0, 4, 8, 4), VerticalAlignment = VerticalAlignment.Stretch, }; Grid.SetColumn(accentBar, 0); grid.Children.Add(accentBar); var txt = new TextBlock { Text = label, FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = foreground, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(txt, 1); grid.Children.Add(txt); child = grid; } else { child = new TextBlock { Text = label, FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = foreground, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }; } var btn = new Border { MinWidth = 84, Height = 36, CornerRadius = new CornerRadius(10), Background = background, BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(14, 0, 14, 0), Margin = new Thickness(10, 0, 0, 0), Cursor = Cursors.Hand, Child = child, }; btn.MouseEnter += (_, _) => btn.Opacity = 0.85; btn.MouseLeave += (_, _) => btn.Opacity = 1.0; return btn; } /// 도구 승인 다이얼로그를 표시하고 결과를 반환합니다. internal static string? Show(Window? owner, string message, List options) => Show(owner, message, options, CancellationToken.None); /// /// 도구 승인 다이얼로그를 표시합니다. cancellationToken이 트리거되면 창을 자동으로 닫고 null을 반환합니다. /// 장시간 미응답 시 타임아웃으로 에이전트 루프가 멈추는 것을 방지하기 위해 사용합니다. /// internal static string? Show(Window? owner, string message, List options, CancellationToken cancellationToken) { var dialog = new ToolApprovalWindow(message, options); if (owner != null && IsWindowAlive(owner)) { dialog.WindowStartupLocation = WindowStartupLocation.CenterOwner; dialog.Owner = owner; } CancellationTokenRegistration reg = default; if (cancellationToken.CanBeCanceled) { reg = cancellationToken.Register(() => { // UI 스레드에서 안전하게 닫기 dialog.Dispatcher.BeginInvoke(new Action(() => { try { if (dialog.IsVisible) dialog.Close(); } catch { /* 이미 닫혀있거나 파괴 중 — 무시 */ } })); }); } try { dialog.ShowDialog(); } finally { reg.Dispose(); } return dialog._result; } private static bool IsWindowAlive(Window? w) { if (w == null) return false; try { var _ = w.IsVisible; return true; } catch { return false; } } }