using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using AxCopilot.Models; using AxCopilot.Services; using AxCopilot.Services.Agent; namespace AxCopilot.Views; public partial class ChatWindow { // ── A-2: PermissionItems 이벤트 위임 ── private bool _permPanelDelegationInitialized; private sealed class PermissionItemTag { public required string Level { get; init; } public required bool IsActive { get; init; } public required Brush SelectedBackground { get; init; } public required Brush HoverBackground { get; init; } } private void InitPermissionPanelDelegation() { if (_permPanelDelegationInitialized || PermissionItems == null) return; _permPanelDelegationInitialized = true; PermissionItems.MouseMove += PermissionItems_DelegatedMouseMove; PermissionItems.MouseLeave += PermissionItems_DelegatedMouseLeave; PermissionItems.PreviewMouseLeftButtonDown += PermissionItems_DelegatedLeftButtonDown; PermissionItems.PreviewKeyDown += PermissionItems_DelegatedKeyDown; } private Border? _lastHoveredPermBorder; private void PermissionItems_DelegatedMouseMove(object sender, MouseEventArgs e) { var border = FindAncestorWithTag(e.OriginalSource as DependencyObject); if (ReferenceEquals(border, _lastHoveredPermBorder)) return; if (_lastHoveredPermBorder?.Tag is PermissionItemTag prevTag) _lastHoveredPermBorder.Background = prevTag.IsActive ? prevTag.SelectedBackground : Brushes.Transparent; _lastHoveredPermBorder = border; if (border?.Tag is PermissionItemTag tag) border.Background = tag.IsActive ? tag.SelectedBackground : tag.HoverBackground; } private void PermissionItems_DelegatedMouseLeave(object sender, MouseEventArgs e) { if (_lastHoveredPermBorder?.Tag is PermissionItemTag prevTag) _lastHoveredPermBorder.Background = prevTag.IsActive ? prevTag.SelectedBackground : Brushes.Transparent; _lastHoveredPermBorder = null; } private void PermissionItems_DelegatedLeftButtonDown(object sender, MouseButtonEventArgs e) { var border = FindAncestorWithTag(e.OriginalSource as DependencyObject); if (border?.Tag is PermissionItemTag tag) { e.Handled = true; ApplyPermissionLevel(tag.Level); } } private void PermissionItems_DelegatedKeyDown(object sender, KeyEventArgs e) { if (e.Key is not (Key.Enter or Key.Space)) return; var border = FindAncestorWithTag(e.OriginalSource as DependencyObject); if (border?.Tag is PermissionItemTag tag) { e.Handled = true; ApplyPermissionLevel(tag.Level); } } private void ApplyPermissionLevel(string level) { _settings.Settings.Llm.FilePermission = PermissionModeCatalog.NormalizeGlobalMode(level); ScheduleSettingsSave(); _appState.LoadFromSettings(_settings); UpdatePermissionUI(); SaveConversationSettings(); RefreshInlineSettingsPanel(); RefreshOverlayModeButtons(); PermissionPopup.IsOpen = false; } private void BtnPermission_Click(object sender, RoutedEventArgs e) { if (PermissionPopup == null) return; // Dynamically retarget popup to whichever button was clicked (FolderBar or Inline) if (sender is UIElement clickedElement && (ReferenceEquals(clickedElement, BtnPermissionInline) || ReferenceEquals(clickedElement, BtnPermission))) { PermissionPopup.PlacementTarget = clickedElement; } InitPermissionPanelDelegation(); _lastHoveredPermBorder = null; PermissionItems.Children.Clear(); ChatConversation? currentConversation; lock (_convLock) currentConversation = _currentConversation; var coreLevels = PermissionModePresentationCatalog.Ordered.ToList(); var current = PermissionModeCatalog.NormalizeGlobalMode(_settings.Settings.Llm.FilePermission); void AddPermissionRows(Panel container, IEnumerable levels) { var hoverBackground = TryFindResource("ItemHoverBackground") as Brush ?? BrushFromHex("#F8FAFC"); var selectedBackground = TryFindResource("HintBackground") as Brush ?? BrushFromHex("#F8FAFC"); var selectedBorder = TryFindResource("AccentColor") as Brush ?? BrushFromHex("#D6E4FF"); foreach (var item in levels) { var level = item.Mode; var isActive = level.Equals(current, StringComparison.OrdinalIgnoreCase); var rowBorder = new Border { Background = isActive ? selectedBackground : Brushes.Transparent, BorderBrush = isActive ? selectedBorder : Brushes.Transparent, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(12), Padding = new Thickness(10, 10, 10, 10), Margin = new Thickness(0, 0, 0, 4), Cursor = Cursors.Hand, Focusable = true, }; KeyboardNavigation.SetIsTabStop(rowBorder, true); var row = new Grid(); row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); row.Children.Add(new TextBlock { Text = item.Icon, FontFamily = s_segoeIconFont, FontSize = 15, Foreground = BrushFromHex(item.ColorHex), Margin = new Thickness(0, 0, 10, 0), VerticalAlignment = VerticalAlignment.Center, }); var textStack = new StackPanel(); textStack.Children.Add(new TextBlock { Text = item.Title, FontSize = 13.5, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White, }); textStack.Children.Add(new TextBlock { Text = item.Description, FontSize = 11.5, Margin = new Thickness(0, 2, 0, 0), Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, TextWrapping = TextWrapping.Wrap, LineHeight = 16, MaxWidth = 220, }); Grid.SetColumn(textStack, 1); row.Children.Add(textStack); var check = new TextBlock { Text = isActive ? "\uE73E" : "", FontFamily = s_segoeIconFont, FontSize = 12, FontWeight = FontWeights.Bold, Foreground = BrushFromHex("#2563EB"), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(12, 0, 0, 0), }; Grid.SetColumn(check, 2); row.Children.Add(check); rowBorder.Child = row; // A-2: 이벤트 위임 — 개별 람다 대신 Tag에 메타 저장 rowBorder.Tag = new PermissionItemTag { Level = level, IsActive = isActive, SelectedBackground = selectedBackground, HoverBackground = hoverBackground, }; container.Children.Add(rowBorder); } } AddPermissionRows(PermissionItems, coreLevels); PermissionPopup.IsOpen = true; Dispatcher.BeginInvoke(() => { TryFocusFirstPermissionElement(PermissionItems); }, System.Windows.Threading.DispatcherPriority.Input); } private static bool TryFocusFirstPermissionElement(DependencyObject root) { if (root is UIElement ui && ui.Focusable && ui.IsEnabled && ui.Visibility == Visibility.Visible) return ui.Focus(); var childCount = VisualTreeHelper.GetChildrenCount(root); for (var i = 0; i < childCount; i++) { var child = VisualTreeHelper.GetChild(root, i); if (TryFocusFirstPermissionElement(child)) return true; } return false; } private void SetToolPermissionOverride(string toolName, string? mode) { if (string.IsNullOrWhiteSpace(toolName)) return; var toolPermissions = _settings.Settings.Llm.ToolPermissions ??= new Dictionary(StringComparer.OrdinalIgnoreCase); var existingKey = toolPermissions.Keys.FirstOrDefault(x => string.Equals(x, toolName, StringComparison.OrdinalIgnoreCase)); if (string.IsNullOrWhiteSpace(mode)) { if (!string.IsNullOrWhiteSpace(existingKey)) toolPermissions.Remove(existingKey!); } else { toolPermissions[existingKey ?? toolName] = PermissionModeCatalog.NormalizeToolOverride(mode); } ScheduleSettingsSave(); _appState.LoadFromSettings(_settings); UpdatePermissionUI(); SaveConversationSettings(); } private void RefreshPermissionPopup() { if (PermissionPopup == null) return; BtnPermission_Click(this, new RoutedEventArgs()); } private bool GetPermissionPopupSectionExpanded(string sectionKey, bool defaultValue = false) { var map = _settings.Settings.Llm.PermissionPopupSections; if (map != null && map.TryGetValue(sectionKey, out var expanded)) return expanded; return defaultValue; } private void SetPermissionPopupSectionExpanded(string sectionKey, bool expanded) { var map = _settings.Settings.Llm.PermissionPopupSections ??= new Dictionary(StringComparer.OrdinalIgnoreCase); map[sectionKey] = expanded; ScheduleSettingsSave(); } /// Shift+Tab으로 권한 모드를 순환합니다 (Claude Code 스타일). private void CyclePermissionMode() { var llm = _settings.Settings.Llm; llm.FilePermission = NextPermission(llm.FilePermission); ScheduleSettingsSave(); _appState.LoadFromSettings(_settings); UpdatePermissionUI(); SaveConversationSettings(); RefreshInlineSettingsPanel(); // Toast 알림 var label = PermissionModeCatalog.ToDisplayLabel(llm.FilePermission); var icon = PermissionModeCatalog.NormalizeGlobalMode(llm.FilePermission) switch { "Plan" => "\uE769", "AcceptEdits" => "\uE73E", "BypassPermissions" => "\uE7BA", "Deny" => "\uE711", _ => "\uE8D7", }; ShowToast(label, icon); } private void PlanModeBannerClose_Click(object sender, MouseButtonEventArgs e) { if (PlanModeBanner != null) PlanModeBanner.Visibility = Visibility.Collapsed; } private void BtnPermissionTopBannerClose_Click(object sender, RoutedEventArgs e) { if (PermissionTopBanner != null) PermissionTopBanner.Visibility = Visibility.Collapsed; } private void UpdatePermissionUI() { if (PermissionLabel == null || PermissionIcon == null) return; // 계획 모드 배너 기본 숨김 — Plan 분기에서만 표시 if (PlanModeBanner != null) PlanModeBanner.Visibility = Visibility.Collapsed; ChatConversation? currentConversation; lock (_convLock) currentConversation = _currentConversation; var summary = _appState.GetPermissionSummary(currentConversation); var perm = PermissionModeCatalog.NormalizeGlobalMode(summary.EffectiveMode); PermissionLabel.Text = PermissionModeCatalog.ToDisplayLabel(perm); if (PermissionLabelInline != null) PermissionLabelInline.Text = PermissionLabel.Text; PermissionIcon.Text = perm switch { "AcceptEdits" => "\uE73E", "Plan" => "\uE769", "BypassPermissions" => "\uE7BA", "Deny" => "\uE711", _ => "\uE8D7", }; if (PermissionIconInline != null) PermissionIconInline.Text = PermissionIcon.Text; if (BtnPermission != null) { var operationMode = OperationModePolicy.Normalize(_settings.Settings.OperationMode); BtnPermission.ToolTip = $"{summary.Description}\n운영 모드: {operationMode}\n기본값: {PermissionModeCatalog.ToDisplayLabel(summary.DefaultMode)} · 예외 {summary.OverrideCount}개"; BtnPermission.Background = Brushes.Transparent; BtnPermission.BorderThickness = new Thickness(1); } if (!string.Equals(_lastPermissionBannerMode, perm, StringComparison.OrdinalIgnoreCase)) _lastPermissionBannerMode = perm; if (perm == PermissionModeCatalog.AcceptEdits) { var activeColor = new SolidColorBrush(Color.FromRgb(0x10, 0x7C, 0x10)); PermissionLabel.Foreground = activeColor; if (PermissionLabelInline != null) PermissionLabelInline.Foreground = activeColor; PermissionIcon.Foreground = activeColor; if (PermissionIconInline != null) PermissionIconInline.Foreground = activeColor; if (BtnPermission != null) BtnPermission.BorderBrush = BrushFromHex("#86EFAC"); if (PermissionTopBanner != null) { PermissionTopBanner.BorderBrush = BrushFromHex("#86EFAC"); PermissionTopBannerIcon.Text = "\uE73E"; PermissionTopBannerIcon.Foreground = activeColor; PermissionTopBannerTitle.Text = "현재 권한 모드 · 편집 자동 승인"; PermissionTopBannerTitle.Foreground = BrushFromHex("#166534"); PermissionTopBannerText.Text = "모든 파일 편집은 자동 승인하고, 명령 실행만 계속 확인합니다."; PermissionTopBanner.Visibility = Visibility.Collapsed; } } else if (perm == PermissionModeCatalog.Deny) { var denyColor = new SolidColorBrush(Color.FromRgb(0x10, 0x7C, 0x10)); PermissionLabel.Foreground = denyColor; if (PermissionLabelInline != null) PermissionLabelInline.Foreground = denyColor; PermissionIcon.Foreground = denyColor; if (PermissionIconInline != null) PermissionIconInline.Foreground = denyColor; if (BtnPermission != null) BtnPermission.BorderBrush = BrushFromHex("#86EFAC"); if (PermissionTopBanner != null) { PermissionTopBanner.BorderBrush = BrushFromHex("#86EFAC"); PermissionTopBannerIcon.Text = "\uE73E"; PermissionTopBannerIcon.Foreground = denyColor; PermissionTopBannerTitle.Text = "현재 권한 모드 · 읽기 전용"; PermissionTopBannerTitle.Foreground = denyColor; PermissionTopBannerText.Text = "기존 파일은 읽기만 가능하며 수정/삭제가 차단되고, 새 파일 생성은 가능합니다."; PermissionTopBanner.Visibility = Visibility.Collapsed; } } else if (perm == PermissionModeCatalog.Plan) { var planColor = new SolidColorBrush(Color.FromRgb(0xD9, 0x77, 0x06)); PermissionLabel.Foreground = planColor; if (PermissionLabelInline != null) PermissionLabelInline.Foreground = planColor; PermissionIcon.Foreground = planColor; if (PermissionIconInline != null) PermissionIconInline.Foreground = planColor; if (BtnPermission != null) BtnPermission.BorderBrush = BrushFromHex("#FDE68A"); if (PermissionTopBanner != null) { PermissionTopBanner.BorderBrush = BrushFromHex("#FDE68A"); PermissionTopBannerIcon.Text = "\uE769"; PermissionTopBannerIcon.Foreground = planColor; PermissionTopBannerTitle.Text = "현재 권한 모드 · 계획 모드"; PermissionTopBannerTitle.Foreground = planColor; PermissionTopBannerText.Text = "파일을 읽고 분석한 뒤, 실행 전에 계획을 먼저 보여줍니다."; PermissionTopBanner.Visibility = Visibility.Collapsed; } // 계획 모드 배너 표시 if (PlanModeBanner != null) PlanModeBanner.Visibility = Visibility.Visible; } else if (perm == PermissionModeCatalog.BypassPermissions) { var autoColor = new SolidColorBrush(Color.FromRgb(0xC2, 0x41, 0x0C)); PermissionLabel.Foreground = autoColor; if (PermissionLabelInline != null) PermissionLabelInline.Foreground = autoColor; PermissionIcon.Foreground = autoColor; if (PermissionIconInline != null) PermissionIconInline.Foreground = autoColor; if (BtnPermission != null) BtnPermission.BorderBrush = BrushFromHex("#FDBA74"); if (PermissionTopBanner != null) { PermissionTopBanner.BorderBrush = BrushFromHex("#FDBA74"); PermissionTopBannerIcon.Text = "\uE814"; PermissionTopBannerIcon.Foreground = autoColor; PermissionTopBannerTitle.Text = "현재 권한 모드 · 권한 건너뛰기"; PermissionTopBannerTitle.Foreground = autoColor; PermissionTopBannerText.Text = "파일 편집과 명령 실행까지 모두 자동 허용합니다. 민감한 작업 전에는 설정을 다시 확인하세요."; PermissionTopBanner.Visibility = Visibility.Collapsed; } } else { var defaultFg = BrushFromHex("#2563EB"); var iconFg = new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xEB)); PermissionLabel.Foreground = defaultFg; if (PermissionLabelInline != null) PermissionLabelInline.Foreground = defaultFg; PermissionIcon.Foreground = iconFg; if (PermissionIconInline != null) PermissionIconInline.Foreground = iconFg; if (BtnPermission != null) BtnPermission.BorderBrush = BrushFromHex("#BFDBFE"); if (PermissionTopBanner != null) { if (perm == PermissionModeCatalog.Default) { PermissionTopBanner.BorderBrush = BrushFromHex("#BFDBFE"); PermissionTopBannerIcon.Text = "\uE8D7"; PermissionTopBannerIcon.Foreground = BrushFromHex("#1D4ED8"); PermissionTopBannerTitle.Text = "현재 권한 모드 · 권한 요청"; PermissionTopBannerTitle.Foreground = BrushFromHex("#1D4ED8"); PermissionTopBannerText.Text = "변경하기 전에 항상 확인합니다."; PermissionTopBanner.Visibility = Visibility.Collapsed; } else { PermissionTopBanner.Visibility = Visibility.Collapsed; } } } } }