using System.Linq; using System.Windows.Data; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using AxCopilot.Services; using AxCopilot.ViewModels; namespace AxCopilot.Views; public partial class SettingsWindow : Window { private const string UnifiedAdminPassword = "axgo123!"; private readonly SettingsViewModel _vm; private readonly Action _previewCallback; private readonly Action _revertCallback; private bool _saved; private bool _isDisplayModeSyncing; /// /// 핫키 녹화 시작/종료를 외부(App.xaml.cs)에 알리는 콜백. /// true = 녹화 시작(핫키 일시 정지), false = 녹화 종료(핫키 재개) /// public Action? SuspendHotkeyCallback { get; set; } /// 테마 키를 받아 런처에 즉시 미리보기 적용 /// 취소/X 닫기 시 원래 설정으로 복원 public SettingsWindow(SettingsViewModel vm, Action previewCallback, Action revertCallback) { InitializeComponent(); _vm = vm; _previewCallback = previewCallback; _revertCallback = revertCallback; DataContext = vm; if (MainSettingsTab != null && AgentTabItem != null && MainSettingsTab.Items.Contains(AgentTabItem)) MainSettingsTab.Items.Remove(AgentTabItem); if (MainSettingsTab != null && AgentShortcutTabItem != null && MainSettingsTab.Items.Contains(AgentShortcutTabItem)) { MainSettingsTab.Items.Remove(AgentShortcutTabItem); MainSettingsTab.Items.Add(AgentShortcutTabItem); } vm.ThemePreviewRequested += (_, _) => _previewCallback(vm.SelectedThemeKey); vm.SaveCompleted += (_, _) => { _saved = true; Close(); // 인덱스 재빌드를 백그라운드에서 조용히 실행 (UI 차단 없음) _ = Task.Run(async () => { try { var indexService = ((System.Windows.Application.Current) as App)?.IndexService; if (indexService != null) await indexService.BuildAsync(); } catch { /* 인덱싱 실패해도 설정 저장은 완료 */ } }); }; Loaded += async (_, _) => { RefreshHotkeyBadges(); SetVersionText(); EnsureHotkeyInCombo(); BuildQuoteCategoryCheckboxes(); BuildDockBarSettings(); BuildTextActionCommandsPanel(); if (HasLegacyAgentTab()) { MoveBlockSectionToEtc(); BuildServiceModelPanels(); } // 스킬이 아직 로드되지 않았으면 백그라운드에서 로드 후 UI 구성 var app = System.Windows.Application.Current as App; var skillsLoaded = Services.Agent.SkillService.Skills.Count > 0; if (!skillsLoaded && (app?.SettingsService?.Settings.Llm.EnableSkillSystem ?? false)) { // 스킬 로드 (RuntimeDetector 포함)를 백그라운드에서 실행 await Task.Run(() => { Services.Agent.SkillService.EnsureSkillFolder(); Services.Agent.SkillService.LoadSkills(app?.SettingsService?.Settings.Llm.SkillsFolderPath); }); } if (HasLegacyAgentTab()) { BuildToolRegistryPanel(); LoadAdvancedSettings(); } RefreshStorageInfo(); // 개발자 모드는 저장된 설정 유지 (끄면 하위 기능 모두 비활성) UpdateDevModeContentVisibility(); // AI 기능 토글 초기화 ApplyAiEnabledState(app?.SettingsService?.Settings.AiEnabled ?? false, init: true); ApplyOperationModeState(app?.SettingsService?.Settings.OperationMode); InitializeDisplayModeUi(); if (HasLegacyAgentTab()) SyncAgentSelectionCards(); }; } private bool HasLegacyAgentTab() => MainSettingsTab != null && AgentTabItem != null && MainSettingsTab.Items.Contains(AgentTabItem); private void SyncAgentSelectionCards() { var service = (_vm.LlmService ?? "").Trim().ToLowerInvariant(); if (AgentServiceCardOllama != null) AgentServiceCardOllama.IsChecked = service == "ollama"; if (AgentServiceCardVllm != null) AgentServiceCardVllm.IsChecked = service == "vllm"; if (AgentServiceCardGemini != null) AgentServiceCardGemini.IsChecked = service == "gemini"; if (AgentServiceCardClaude != null) AgentServiceCardClaude.IsChecked = service == "claude"; var permission = Services.Agent.PermissionModeCatalog.NormalizeGlobalMode(_vm.DefaultAgentPermission); if (AgentPermissionCardAsk != null) AgentPermissionCardAsk.IsChecked = permission == "Ask"; if (AgentPermissionCardPlan != null) AgentPermissionCardPlan.IsChecked = permission == "Plan"; if (AgentPermissionCardAuto != null) AgentPermissionCardAuto.IsChecked = permission == "Auto"; if (AgentPermissionCardDeny != null) AgentPermissionCardDeny.IsChecked = permission == "Deny"; var decision = (_vm.AgentDecisionLevel ?? "normal").Trim().ToLowerInvariant(); if (AgentDecisionCardMinimal != null) AgentDecisionCardMinimal.IsChecked = decision == "minimal"; if (AgentDecisionCardNormal != null) AgentDecisionCardNormal.IsChecked = decision == "normal"; if (AgentDecisionCardDetailed != null) AgentDecisionCardDetailed.IsChecked = decision == "detailed"; var operationMode = OperationModePolicy.Normalize(_vm.OperationMode); if (AgentOperationModeInternal != null) AgentOperationModeInternal.IsChecked = operationMode == OperationModePolicy.InternalMode; if (AgentOperationModeExternal != null) AgentOperationModeExternal.IsChecked = operationMode == OperationModePolicy.ExternalMode; var maxContextTokens = _vm.LlmMaxContextTokens; if (AgentContextTokens4K != null) AgentContextTokens4K.IsChecked = maxContextTokens <= 4096; if (AgentContextTokens16K != null) AgentContextTokens16K.IsChecked = maxContextTokens > 4096 && maxContextTokens <= 16384; if (AgentContextTokens64K != null) AgentContextTokens64K.IsChecked = maxContextTokens > 16384 && maxContextTokens <= 65536; if (AgentContextTokens256K != null) AgentContextTokens256K.IsChecked = maxContextTokens > 65536 && maxContextTokens <= 262144; if (AgentContextTokens1M != null) AgentContextTokens1M.IsChecked = maxContextTokens > 262144; var retentionDays = _vm.LlmRetentionDays; if (AgentRetentionDays7 != null) AgentRetentionDays7.IsChecked = retentionDays == 7; if (AgentRetentionDays30 != null) AgentRetentionDays30.IsChecked = retentionDays == 30; if (AgentRetentionDays90 != null) AgentRetentionDays90.IsChecked = retentionDays == 90; if (AgentRetentionDaysUnlimited != null) AgentRetentionDaysUnlimited.IsChecked = retentionDays == 0; var logLevel = (_vm.AgentLogLevel ?? "simple").Trim().ToLowerInvariant(); if (AgentLogLevelSimple != null) AgentLogLevelSimple.IsChecked = logLevel == "simple"; if (AgentLogLevelDetailed != null) AgentLogLevelDetailed.IsChecked = logLevel == "detailed"; if (AgentLogLevelDebug != null) AgentLogLevelDebug.IsChecked = logLevel == "debug"; } private void InitializeDisplayModeUi() { var app = System.Windows.Application.Current as App; if (app?.SettingsService?.Settings?.Llm != null) app.SettingsService.Settings.Llm.AgentUiExpressionLevel = "rich"; SetDisplayMode("rich", persist: false); } private void DisplayMode_Checked(object sender, RoutedEventArgs e) { if (_isDisplayModeSyncing) return; var rb = sender as RadioButton; var next = rb?.Name switch { "DisplayModeRich" => "rich", "DisplayModeSimple" => "simple", _ => "balanced", }; SetDisplayMode(next, persist: true); } private void AgentDisplayMode_Checked(object sender, RoutedEventArgs e) { if (_isDisplayModeSyncing) return; SetDisplayMode("rich", persist: true); } private void SetDisplayMode(string mode, bool persist) { mode = NormalizeDisplayMode(mode); _isDisplayModeSyncing = true; try { var rich = GetDisplayModeRadio("DisplayModeRich"); var balanced = GetDisplayModeRadio("DisplayModeBalanced"); var simple = GetDisplayModeRadio("DisplayModeSimple"); var agentRich = GetDisplayModeRadio("AgentDisplayModeRich"); var agentBalanced = GetDisplayModeRadio("AgentDisplayModeBalanced"); var agentSimple = GetDisplayModeRadio("AgentDisplayModeSimple"); if (rich != null) rich.IsChecked = mode == "rich"; if (balanced != null) balanced.IsChecked = mode == "balanced"; if (simple != null) simple.IsChecked = mode == "simple"; if (agentRich != null) agentRich.IsChecked = mode == "rich"; if (agentBalanced != null) agentBalanced.IsChecked = mode == "balanced"; if (agentSimple != null) agentSimple.IsChecked = mode == "simple"; } finally { _isDisplayModeSyncing = false; } ApplyMainTabVisibility(mode); if (HasLegacyAgentTab()) ApplyAgentSubTabVisibility(mode); if (!persist) return; var app = System.Windows.Application.Current as App; if (app?.SettingsService?.Settings?.Llm == null) return; app.SettingsService.Settings.Llm.AgentUiExpressionLevel = mode; app.SettingsService.Save(); } private void ApplyMainTabVisibility(string mode) { if (MainSettingsTab == null) return; var simpleKeep = new HashSet(StringComparer.OrdinalIgnoreCase) { "일반", "테마", "기능" }; var balancedKeep = new HashSet(StringComparer.OrdinalIgnoreCase) { "일반", "테마", "클립보드", "캡처", "시스템", "기능" }; foreach (var item in MainSettingsTab.Items.OfType()) { var header = item.Header?.ToString() ?? ""; var visible = mode switch { "simple" => simpleKeep.Contains(header), "balanced" => balancedKeep.Contains(header), _ => true, }; if (string.Equals(header, "알림", StringComparison.OrdinalIgnoreCase)) visible = false; item.Visibility = visible ? Visibility.Visible : Visibility.Collapsed; } if (MainSettingsTab.SelectedItem is TabItem selected && selected.Visibility == Visibility.Visible) return; var firstVisible = MainSettingsTab.Items.OfType().FirstOrDefault(t => t.Visibility == Visibility.Visible); if (firstVisible != null) MainSettingsTab.SelectedItem = firstVisible; } private void ApplyAgentSubTabVisibility(string mode) { if (AgentTabCommon == null) return; SetSubTabVisible(AgentTabCommon, true); SetSubTabVisible(AgentTabChat, true); SetSubTabVisible(AgentTabCoworkCode, true); SetSubTabVisible(AgentTabDev, true); SetSubTabVisible(AgentTabCowork, false); SetSubTabVisible(AgentTabCode, false); SetSubTabVisible(AgentTabTools, false); SetSubTabVisible(AgentTabEtc, false); if (AgentTabCommon.IsChecked != true && AgentTabChat.IsChecked != true && AgentTabCoworkCode.IsChecked != true && AgentTabDev.IsChecked != true) { AgentTabCommon.IsChecked = true; } AgentSubTab_Checked(this, new RoutedEventArgs()); } private static void SetSubTabVisible(RadioButton? tab, bool visible) { if (tab == null) return; tab.Visibility = visible ? Visibility.Visible : Visibility.Collapsed; if (!visible) tab.IsChecked = false; } private static string NormalizeDisplayMode(string? mode) { return (mode ?? "balanced").Trim().ToLowerInvariant() switch { "rich" => "rich", "simple" => "simple", _ => "balanced", }; } private RadioButton? GetDisplayModeRadio(string name) { if (MainSettingsTab?.Template == null) return null; return MainSettingsTab.Template.FindName(name, MainSettingsTab) as RadioButton; } // ─── 에이전트 차단 섹션 → 기타 탭 이동 ────────────────────────────────────── private void MoveBlockSectionToEtc() { if (AgentBlockSection == null || AgentEtcContent == null) return; var parent = AgentBlockSection.Parent as Panel; if (parent != null) { parent.Children.Remove(AgentBlockSection); AgentBlockSection.Margin = new Thickness(0, 0, 0, 16); AgentEtcContent.Children.Add(AgentBlockSection); } } // ─── 접기/열기 가능 섹션 헬퍼 ────────────────────────────────────────── private Border CreateCollapsibleSection(string title, UIElement content, bool expanded = true) { var headerColor = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#AAAACC")); var section = new StackPanel(); var contentPanel = new StackPanel { Visibility = expanded ? Visibility.Visible : Visibility.Collapsed }; if (content is Panel panel) { // 패널의 자식들을 contentPanel로 옮김 (StackPanel 등) contentPanel.Children.Add(content); } else { contentPanel.Children.Add(content); } var arrow = new TextBlock { Text = "\uE70D", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 9, Foreground = headerColor, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), RenderTransformOrigin = new Point(0.5, 0.5), RenderTransform = new RotateTransform(expanded ? 90 : 0), }; var titleBlock = new TextBlock { Text = title, FontSize = 10.5, FontWeight = FontWeights.SemiBold, Foreground = headerColor, VerticalAlignment = VerticalAlignment.Center, }; var headerPanel = new StackPanel { Orientation = Orientation.Horizontal }; headerPanel.Children.Add(arrow); headerPanel.Children.Add(titleBlock); var header = new Border { Background = Brushes.Transparent, Cursor = Cursors.Hand, Padding = new Thickness(2, 10, 2, 6), Child = headerPanel, }; header.MouseLeftButtonUp += (s, e) => { bool isNowVisible = contentPanel.Visibility == Visibility.Visible; contentPanel.Visibility = isNowVisible ? Visibility.Collapsed : Visibility.Visible; arrow.RenderTransform = new RotateTransform(isNowVisible ? 0 : 90); }; header.MouseEnter += (s, _) => titleBlock.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#CCCCEE")); header.MouseLeave += (s, _) => titleBlock.Foreground = headerColor; section.Children.Add(header); section.Children.Add(contentPanel); return new Border { Child = section, Margin = new Thickness(0, 2, 0, 0), }; } private Border CreateCollapsibleSection(string title, IEnumerable children, bool expanded = true) { var contentPanel = new StackPanel(); foreach (var child in children) contentPanel.Children.Add(child); return CreateCollapsibleSection(title, contentPanel, expanded); } // ─── 도구/커넥터 관리 UI (기타 탭) ───────────────────────────────────────────── private void BuildToolRegistryPanel() { if (AgentEtcContent == null) return; // 도구 목록 데이터 (카테고리별) var toolGroups = new (string Category, string Icon, string IconColor, (string Name, string Desc)[] Tools)[] { ("파일/검색", "\uE8B7", "#F59E0B", new[] { ("file_read", "파일 읽기 ? 텍스트/바이너리 파일의 내용을 읽습니다"), ("file_write", "파일 쓰기 ? 새 파일을 생성하거나 기존 파일을 덮어씁니다"), ("file_edit", "파일 편집 ? 줄 번호 기반으로 파일의 특정 부분을 수정합니다"), ("glob", "파일 패턴 검색 ? 글로브 패턴으로 파일을 찾습니다 (예: **/*.cs)"), ("grep_tool", "텍스트 검색 ? 정규식으로 파일 내용을 검색합니다"), ("folder_map", "폴더 구조 ? 프로젝트의 디렉토리 트리를 조회합니다"), ("document_read", "문서 읽기 ? PDF, DOCX 등 문서 파일의 텍스트를 추출합니다"), }), ("프로세스/빌드", "\uE756", "#06B6D4", new[] { ("process", "프로세스 실행 ? 외부 명령/프로그램을 실행합니다"), ("build_run", "빌드/테스트 ? 프로젝트 타입을 감지하여 빌드/테스트를 실행합니다"), ("dev_env_detect", "개발 환경 감지 ? IDE, 런타임, 빌드 도구를 자동으로 인식합니다"), }), ("코드 분석", "\uE943", "#818CF8", new[] { ("search_codebase", "코드 키워드 검색 ? TF-IDF 기반으로 관련 코드를 찾습니다"), ("code_review", "AI 코드 리뷰 ? Git diff 분석, 파일 정적 검사, PR 요약"), ("lsp", "LSP 인텔리전스 ? 정의 이동, 참조 검색, 심볼 목록 (C#/TS/Python)"), ("test_loop", "테스트 루프 ? 테스트 생성/실행/분석 자동화"), ("git_tool", "Git 작업 ? status, log, diff, commit 등 Git 명령 실행"), ("snippet_runner", "코드 실행 ? C#/Python/JavaScript 스니펫 즉시 실행"), }), ("문서 생성", "\uE8A5", "#34D399", new[] { ("excel_create", "Excel 생성 ? .xlsx 스프레드시트를 생성합니다"), ("docx_create", "Word 생성 ? .docx 문서를 생성합니다"), ("csv_create", "CSV 생성 ? CSV 파일을 생성합니다"), ("markdown_create", "마크다운 생성 ? .md 파일을 생성합니다"), ("html_create", "HTML 생성 ? HTML 파일을 생성합니다"), ("chart_create", "차트 생성 ? 데이터 시각화 차트를 생성합니다"), ("batch_create", "배치 생성 ? 스크립트 파일을 생성합니다"), ("document_review", "문서 검증 ? 문서 품질을 검사합니다"), ("format_convert", "포맷 변환 ? 문서 형식을 변환합니다"), }), ("데이터 처리", "\uE9F5", "#F59E0B", new[] { ("json_tool", "JSON 처리 ? JSON 파싱, 변환, 검증, 포맷팅"), ("regex_tool", "정규식 ? 정규식 테스트, 추출, 치환"), ("diff_tool", "텍스트 비교 ? 두 파일/텍스트 비교, 통합 diff 출력"), ("base64_tool", "인코딩 ? Base64/URL 인코딩, 디코딩"), ("hash_tool", "해시 계산 ? MD5/SHA256 해시 계산 (파일/텍스트)"), ("datetime_tool", "날짜/시간 ? 날짜 변환, 타임존, 기간 계산"), }), ("시스템/환경", "\uE770", "#06B6D4", new[] { ("clipboard_tool", "클립보드 ? Windows 클립보드 읽기/쓰기 (텍스트/이미지)"), ("notify_tool", "알림 ? Windows 시스템 알림 전송"), ("env_tool", "환경변수 ? 환경변수 읽기/설정 (프로세스 범위)"), ("zip_tool", "압축 ? 파일 압축(zip) / 해제"), ("http_tool", "HTTP ? 로컬/사내 HTTP API 호출 (GET/POST)"), ("sql_tool", "SQLite ? SQLite DB 쿼리 실행 (로컬 파일)"), }), ("에이전트", "\uE99A", "#F472B6", new[] { ("spawn_agent", "서브에이전트 ? 하위 작업을 병렬로 실행하는 서브에이전트를 생성합니다"), ("wait_agents", "에이전트 대기 ? 실행 중인 서브에이전트의 결과를 수집합니다"), ("memory", "에이전트 메모리 ? 프로젝트 규칙, 선호도를 저장/검색합니다"), ("skill_manager", "스킬 관리 ? 스킬 목록 조회, 상세 정보, 재로드"), ("project_rules", "프로젝트 지침 ? AGENTS.md 개발 지침을 읽고 씁니다"), }), }; // 도구 목록을 섹션으로 그룹화 var toolCards = new List(); // 설명 // 도구 헤더 toolCards.Add(new TextBlock { Text = $"등록된 도구/커넥터 ({toolGroups.Sum(g => g.Tools.Length)})", FontSize = 13, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White, Margin = new Thickness(2, 4, 0, 4), }); toolCards.Add(new TextBlock { Text = "에이전트가 사용할 수 있는 도구 목록입니다. 코워크/코드 탭에서 LLM이 자동으로 호출합니다.", FontSize = 11, Foreground = TryFindResource("SecondaryText") as Brush ?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9999BB")), Margin = new Thickness(2, 0, 0, 10), TextWrapping = TextWrapping.Wrap, }); foreach (var group in toolGroups) { // 카테고리 카드 var card = new Border { Background = TryFindResource("ItemBackground") as Brush ?? Brushes.White, CornerRadius = new CornerRadius(10), Padding = new Thickness(14, 10, 14, 10), Margin = new Thickness(0, 0, 0, 6), }; var cardPanel = new StackPanel(); // 도구 아이템 패널 (접기/열기 대상) var toolItemsPanel = new StackPanel { Visibility = Visibility.Collapsed }; // 접기/열기 화살표 var arrow = new TextBlock { Text = "\uE70D", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 9, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), RenderTransformOrigin = new Point(0.5, 0.5), RenderTransform = new RotateTransform(0), }; // 카테고리 헤더 (클릭 가능) var catHeader = new Border { Background = Brushes.Transparent, Cursor = Cursors.Hand, }; var catHeaderPanel = new StackPanel { Orientation = Orientation.Horizontal }; catHeaderPanel.Children.Add(arrow); catHeaderPanel.Children.Add(new TextBlock { Text = group.Icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 14, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(group.IconColor)), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), }); var catTitle = new TextBlock { Text = $"{group.Category} ({group.Tools.Length})", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1A1B2E")), VerticalAlignment = VerticalAlignment.Center, }; catHeaderPanel.Children.Add(catTitle); catHeader.Child = catHeaderPanel; // 클릭 시 접기/열기 토글 catHeader.MouseLeftButtonDown += (s, e) => { e.Handled = true; bool isVisible = toolItemsPanel.Visibility == Visibility.Visible; toolItemsPanel.Visibility = isVisible ? Visibility.Collapsed : Visibility.Visible; arrow.RenderTransform = new RotateTransform(isVisible ? 0 : 90); }; cardPanel.Children.Add(catHeader); // 구분선 toolItemsPanel.Children.Add(new Border { Height = 1, Background = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F0F0F4")), Margin = new Thickness(0, 6, 0, 4), }); // 도구 아이템 foreach (var (name, toolDesc) in group.Tools) { var row = new Grid { Margin = new Thickness(0, 3, 0, 3) }; row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) }); row.ColumnDefinitions.Add(new ColumnDefinition()); var nameBlock = new TextBlock { Text = name, FontSize = 12, FontFamily = new FontFamily("Consolas, Cascadia Code, Segoe UI"), Foreground = TryFindResource("AccentColor") as Brush ?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5EFC")), VerticalAlignment = VerticalAlignment.Center, MinWidth = 130, Margin = new Thickness(4, 0, 12, 0), }; Grid.SetColumn(nameBlock, 0); row.Children.Add(nameBlock); var descBlock = new TextBlock { Text = toolDesc, FontSize = 11.5, Foreground = TryFindResource("SecondaryText") as Brush ?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6666AA")), VerticalAlignment = VerticalAlignment.Center, TextWrapping = TextWrapping.Wrap, }; Grid.SetColumn(descBlock, 1); row.Children.Add(descBlock); toolItemsPanel.Children.Add(row); } cardPanel.Children.Add(toolItemsPanel); card.Child = cardPanel; toolCards.Add(card); } // ── 도구 목록을 직접 추가 (카테고리별 접기/열기) ── foreach (var card in toolCards) AgentEtcContent.Children.Insert(AgentEtcContent.Children.Count, card); int insertIdx = AgentEtcContent.Children.Count; // ── 로드된 스킬 목록 ── BuildSkillListSection(ref insertIdx); } private void BuildSkillListSection(ref int insertIdx) { if (AgentEtcContent == null) return; var skills = Services.Agent.SkillService.Skills; if (skills.Count == 0) return; var accentColor = (Color)ColorConverter.ConvertFromString("#4B5EFC"); var subtleText = (Color)ColorConverter.ConvertFromString("#6666AA"); // 스킬 콘텐츠를 모아서 접기/열기 섹션에 넣기 var skillItems = new List(); // 설명 skillItems.Add(new TextBlock { Text = "/ 명령으로 호출할 수 있는 스킬 목록입니다. 앱 내장 + 사용자 추가 스킬이 포함됩니다.\n" + "(스킬은 사용자가 직접 /명령어를 입력해야 실행됩니다. LLM이 자동 호출하지 않습니다.)", FontSize = 11, Foreground = new SolidColorBrush(subtleText), Margin = new Thickness(2, 0, 0, 10), TextWrapping = TextWrapping.Wrap, }); // 내장 스킬 / 고급 스킬 분류 var builtIn = skills.Where(s => string.IsNullOrEmpty(s.Requires)).ToList(); var advanced = skills.Where(s => !string.IsNullOrEmpty(s.Requires)).ToList(); // 내장 스킬 카드 if (builtIn.Count > 0) { var card = CreateSkillGroupCard("내장 스킬", "\uE768", "#34D399", builtIn); skillItems.Add(card); } // 고급 스킬 (런타임 의존) 카드 if (advanced.Count > 0) { var card = CreateSkillGroupCard("고급 스킬 (런타임 필요)", "\uE9D9", "#A78BFA", advanced); skillItems.Add(card); } // ── 스킬 가져오기/내보내기 버튼 ── var btnPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(2, 4, 0, 10), }; // 가져오기 버튼 var importBtn = new Border { Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5EFC")), CornerRadius = new CornerRadius(8), Padding = new Thickness(14, 7, 14, 7), Margin = new Thickness(0, 0, 8, 0), Cursor = Cursors.Hand, }; var importContent = new StackPanel { Orientation = Orientation.Horizontal }; importContent.Children.Add(new TextBlock { Text = "\uE8B5", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = Brushes.White, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), }); importContent.Children.Add(new TextBlock { Text = "스킬 가져오기 (.zip)", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White, VerticalAlignment = VerticalAlignment.Center, }); importBtn.Child = importContent; importBtn.MouseLeftButtonUp += SkillImport_Click; importBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85; importBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0; btnPanel.Children.Add(importBtn); // 내보내기 버튼 var exportBtn = new Border { Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F0F1F5")), CornerRadius = new CornerRadius(8), Padding = new Thickness(14, 7, 14, 7), Cursor = Cursors.Hand, }; var exportContent = new StackPanel { Orientation = Orientation.Horizontal }; exportContent.Children.Add(new TextBlock { Text = "\uEDE1", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#555")), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), }); exportContent.Children.Add(new TextBlock { Text = "스킬 내보내기", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#444")), VerticalAlignment = VerticalAlignment.Center, }); exportBtn.Child = exportContent; exportBtn.MouseLeftButtonUp += SkillExport_Click; exportBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85; exportBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0; btnPanel.Children.Add(exportBtn); skillItems.Add(btnPanel); // ── 갤러리/통계 링크 버튼 ── var linkPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(2, 0, 0, 12), }; var galleryBtn = new Border { CornerRadius = new CornerRadius(8), Padding = new Thickness(14, 7, 14, 7), Margin = new Thickness(0, 0, 8, 0), Background = new SolidColorBrush(Color.FromArgb(0x18, 0x4B, 0x5E, 0xFC)), Cursor = Cursors.Hand, }; var galleryContent = new StackPanel { Orientation = Orientation.Horizontal }; galleryContent.Children.Add(new TextBlock { Text = "\uE768", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5EFC")), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), }); galleryContent.Children.Add(new TextBlock { Text = "스킬 갤러리 열기", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5EFC")), }); galleryBtn.Child = galleryContent; galleryBtn.MouseLeftButtonUp += (_, _) => { var win = new SkillGalleryWindow(); win.Owner = Window.GetWindow(this); win.ShowDialog(); }; galleryBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.8; galleryBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0; linkPanel.Children.Add(galleryBtn); var statsBtn = new Border { CornerRadius = new CornerRadius(8), Padding = new Thickness(14, 7, 14, 7), Background = new SolidColorBrush(Color.FromArgb(0x18, 0xA7, 0x8B, 0xFA)), Cursor = Cursors.Hand, }; var statsContent = new StackPanel { Orientation = Orientation.Horizontal }; statsContent.Children.Add(new TextBlock { Text = "\uE9D9", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#A78BFA")), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), }); statsContent.Children.Add(new TextBlock { Text = "실행 통계 보기", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#A78BFA")), }); statsBtn.Child = statsContent; statsBtn.MouseLeftButtonUp += (_, _) => { var win = new AgentStatsDashboardWindow(); win.Owner = Window.GetWindow(this); win.ShowDialog(); }; statsBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.8; statsBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0; linkPanel.Children.Add(statsBtn); skillItems.Add(linkPanel); // ── 스킬 섹션 직접 추가 ── // 스킬 헤더 var skillHeader = new TextBlock { Text = $"슬래시 스킬 ({skills.Count})", FontSize = 13, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White, Margin = new Thickness(2, 16, 0, 8), }; AgentEtcContent.Children.Insert(insertIdx++, skillHeader); foreach (var item in skillItems) AgentEtcContent.Children.Insert(insertIdx++, item); } private Border CreateSkillGroupCard(string title, string icon, string colorHex, List skills) { var color = (Color)ColorConverter.ConvertFromString(colorHex); var card = new Border { Background = TryFindResource("ItemBackground") as Brush ?? Brushes.White, CornerRadius = new CornerRadius(10), Padding = new Thickness(14, 10, 14, 10), Margin = new Thickness(0, 0, 0, 6), }; var panel = new StackPanel(); // 스킬 아이템 패널 (접기/열기 대상) var skillItemsPanel = new StackPanel { Visibility = Visibility.Collapsed }; // 접기/열기 화살표 var arrow = new TextBlock { Text = "\uE70D", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 9, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), RenderTransformOrigin = new Point(0.5, 0.5), RenderTransform = new RotateTransform(0), }; // 카테고리 헤더 (클릭 가능) var catHeader = new Border { Background = Brushes.Transparent, Cursor = Cursors.Hand, }; var catHeaderPanel = new StackPanel { Orientation = Orientation.Horizontal }; catHeaderPanel.Children.Add(arrow); catHeaderPanel.Children.Add(new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 14, Foreground = new SolidColorBrush(color), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), }); catHeaderPanel.Children.Add(new TextBlock { Text = $"{title} ({skills.Count})", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1A1B2E")), VerticalAlignment = VerticalAlignment.Center, }); catHeader.Child = catHeaderPanel; // 클릭 시 접기/열기 토글 catHeader.MouseLeftButtonDown += (s, e) => { e.Handled = true; bool isVisible = skillItemsPanel.Visibility == Visibility.Visible; skillItemsPanel.Visibility = isVisible ? Visibility.Collapsed : Visibility.Visible; arrow.RenderTransform = new RotateTransform(isVisible ? 0 : 90); }; panel.Children.Add(catHeader); // 구분선 skillItemsPanel.Children.Add(new Border { Height = 1, Background = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F0F0F4")), Margin = new Thickness(0, 2, 0, 4), }); // 스킬 아이템 foreach (var skill in skills) { var row = new StackPanel { Margin = new Thickness(0, 4, 0, 4) }; // 위 줄: 스킬 명칭 + 가용성 뱃지 var namePanel = new StackPanel { Orientation = Orientation.Horizontal }; namePanel.Children.Add(new TextBlock { Text = $"/{skill.Name}", FontSize = 12, FontFamily = new FontFamily("Consolas, Cascadia Code, Segoe UI"), Foreground = skill.IsAvailable ? (TryFindResource("AccentColor") as Brush ?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5EFC"))) : Brushes.Gray, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(4, 0, 8, 0), Opacity = skill.IsAvailable ? 1.0 : 0.5, }); if (!skill.IsAvailable && !string.IsNullOrEmpty(skill.Requires)) { namePanel.Children.Add(new Border { Background = new SolidColorBrush(Color.FromArgb(0x20, 0xF8, 0x71, 0x71)), CornerRadius = new CornerRadius(4), Padding = new Thickness(5, 1, 5, 1), VerticalAlignment = VerticalAlignment.Center, Child = new TextBlock { Text = skill.UnavailableHint, FontSize = 9, Foreground = new SolidColorBrush(Color.FromRgb(0xF8, 0x71, 0x71)), FontWeight = FontWeights.SemiBold, }, }); } else if (skill.IsAvailable && !string.IsNullOrEmpty(skill.Requires)) { namePanel.Children.Add(new Border { Background = new SolidColorBrush(Color.FromArgb(0x20, 0x34, 0xD3, 0x99)), CornerRadius = new CornerRadius(4), Padding = new Thickness(5, 1, 5, 1), VerticalAlignment = VerticalAlignment.Center, Child = new TextBlock { Text = "? 사용 가능", FontSize = 9, Foreground = new SolidColorBrush(Color.FromRgb(0x34, 0xD3, 0x99)), FontWeight = FontWeights.SemiBold, }, }); } row.Children.Add(namePanel); // 아래 줄: 설명 (뱃지와 구분되도록 위 여백 추가) row.Children.Add(new TextBlock { Text = $"{skill.Label} ? {skill.Description}", FontSize = 11.5, Foreground = TryFindResource("SecondaryText") as Brush ?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6666AA")), TextWrapping = TextWrapping.Wrap, Margin = new Thickness(4, 3, 0, 0), Opacity = skill.IsAvailable ? 1.0 : 0.5, }); skillItemsPanel.Children.Add(row); } panel.Children.Add(skillItemsPanel); card.Child = panel; return card; } // ─── 선택 텍스트 AI 명령 설정 ──────────────────────────────────────────────── private void BuildTextActionCommandsPanel() { if (TextActionCommandsPanel == null) return; TextActionCommandsPanel.Children.Clear(); var svc = (System.Windows.Application.Current as App)?.SettingsService; if (svc == null) return; var enabled = svc.Settings.Launcher.TextActionCommands; var toggleStyle = TryFindResource("ToggleSwitch") as Style; foreach (var (key, label) in TextActionPopup.AvailableCommands) { var row = new Grid { Margin = new Thickness(0, 3, 0, 3) }; row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var labelBlock = new TextBlock { Text = label, FontSize = 12.5, VerticalAlignment = VerticalAlignment.Center, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, }; Grid.SetColumn(labelBlock, 0); row.Children.Add(labelBlock); var capturedKey = key; var cb = new CheckBox { IsChecked = enabled.Contains(key, StringComparer.OrdinalIgnoreCase), HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center, }; if (toggleStyle != null) cb.Style = toggleStyle; cb.Checked += (_, _) => { if (!enabled.Contains(capturedKey)) enabled.Add(capturedKey); svc.Save(); }; cb.Unchecked += (_, _) => { // 최소 1개 유지 if (enabled.Count <= 1) { cb.IsChecked = true; return; } enabled.RemoveAll(x => x == capturedKey); svc.Save(); }; Grid.SetColumn(cb, 1); row.Children.Add(cb); TextActionCommandsPanel.Children.Add(row); } } // ─── 테마 하위 탭 전환 ────────────────────────────────────────────────────── private void ThemeSubTab_Checked(object sender, RoutedEventArgs e) { if (ThemeSelectPanel == null || ThemeColorsPanel == null) return; if (ThemeSubTabSelect?.IsChecked == true) { ThemeSelectPanel.Visibility = Visibility.Visible; ThemeColorsPanel.Visibility = Visibility.Collapsed; } else { ThemeSelectPanel.Visibility = Visibility.Collapsed; ThemeColorsPanel.Visibility = Visibility.Visible; } } // ─── 일반 하위 탭 전환 (일반 + 알림) ────────────────────────────────────── private bool _notifyMoved; private void GeneralSubTab_Checked(object sender, RoutedEventArgs e) { if (GeneralMainPanel == null || GeneralNotifyPanel == null) return; // 알림 탭 내용을 최초 1회 NotifyContent로 이동 if (!_notifyMoved && NotifyContent != null) { MoveNotifyTabContent(); _notifyMoved = true; } GeneralMainPanel.Visibility = GeneralSubTabMain?.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; GeneralNotifyPanel.Visibility = GeneralSubTabNotify?.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; if (GeneralStoragePanel != null) { GeneralStoragePanel.Visibility = GeneralSubTabStorage?.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; if (GeneralSubTabStorage?.IsChecked == true) RefreshStorageInfo2(); } } private void MoveNotifyTabContent() { // XAML의 알림 TabItem에서 ScrollViewer → StackPanel 내용을 NotifyContent로 이동 // 사이드 네비의 "알림" 탭을 찾아서 숨기기 var mainTab = Content as System.Windows.Controls.Grid; var tabControl = mainTab?.Children.OfType().FirstOrDefault() ?? FindVisualChild(this); if (tabControl == null) return; TabItem? notifyTab = null; foreach (TabItem tab in tabControl.Items) { if (tab.Header?.ToString() == "알림") { notifyTab = tab; break; } } if (notifyTab?.Content is ScrollViewer sv && sv.Content is StackPanel sp) { // 내용물을 NotifyContent로 복제 이동 var children = sp.Children.Cast().ToList(); sp.Children.Clear(); foreach (var child in children) NotifyContent.Children.Add(child); } // 알림 탭 숨기기 if (notifyTab != null) notifyTab.Visibility = Visibility.Collapsed; } private static T? FindVisualChild(DependencyObject parent) where T : DependencyObject { for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) { var child = VisualTreeHelper.GetChild(parent, i); if (child is T t) return t; var result = FindVisualChild(child); if (result != null) return result; } return null; } // ─── 독 바 설정 ──────────────────────────────────────────────────────────── private void BuildDockBarSettings() { var svc = (System.Windows.Application.Current as App)?.SettingsService; if (svc == null) return; var launcher = svc.Settings.Launcher; // 토글 바인딩 ChkDockAutoShow.IsChecked = launcher.DockBarAutoShow; ChkDockAutoShow.Checked += (_, _) => { launcher.DockBarAutoShow = true; svc.Save(); (System.Windows.Application.Current as App)?.ToggleDockBar(); }; ChkDockAutoShow.Unchecked += (_, _) => { launcher.DockBarAutoShow = false; svc.Save(); // 끄기 시에는 설정만 저장 ? 독 바를 토글하지 않음 (이미 표시 중이면 유지, 다음 재시작 시 안 뜸) }; ChkDockRainbowGlow.IsChecked = launcher.DockBarRainbowGlow; ChkDockRainbowGlow.Checked += (_, _) => { launcher.DockBarRainbowGlow = true; svc.Save(); RefreshDock(); }; ChkDockRainbowGlow.Unchecked += (_, _) => { launcher.DockBarRainbowGlow = false; svc.Save(); RefreshDock(); }; SliderDockOpacity.Value = launcher.DockBarOpacity; SliderDockOpacity.ValueChanged += (_, e) => { launcher.DockBarOpacity = e.NewValue; svc.Save(); RefreshDock(); }; // 표시 항목 토글 리스트 if (DockItemsPanel == null) return; DockItemsPanel.Children.Clear(); var toggleStyle = TryFindResource("ToggleSwitch") as System.Windows.Style; var enabled = launcher.DockBarItems; foreach (var (key, icon, tooltip) in DockBarWindow.AvailableItems) { var row = new System.Windows.Controls.Grid { Margin = new Thickness(0, 3, 0, 3) }; row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var label = new TextBlock { Text = $"{tooltip}", FontSize = 12.5, VerticalAlignment = VerticalAlignment.Center, Foreground = TryFindResource("PrimaryText") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Black, }; System.Windows.Controls.Grid.SetColumn(label, 0); row.Children.Add(label); var capturedKey = key; var cb = new CheckBox { IsChecked = enabled.Contains(key), HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center, }; if (toggleStyle != null) cb.Style = toggleStyle; cb.Checked += (_, _) => { if (!enabled.Contains(capturedKey)) enabled.Add(capturedKey); svc.Save(); RefreshDock(); }; cb.Unchecked += (_, _) => { enabled.RemoveAll(x => x == capturedKey); svc.Save(); RefreshDock(); }; System.Windows.Controls.Grid.SetColumn(cb, 1); row.Children.Add(cb); DockItemsPanel.Children.Add(row); } } private static SolidColorBrush BrushFromHex(string hex) { try { return new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex)); } catch { return new SolidColorBrush(Colors.Gray); } } private static void RefreshDock() { (System.Windows.Application.Current as App)?.RefreshDockBar(); } private void BtnDockResetPosition_Click(object sender, RoutedEventArgs e) { var result = CustomMessageBox.Show( "독 바를 화면 하단 중앙으로 이동하시겠습니까?", "독 바 위치 초기화", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result != MessageBoxResult.Yes) return; var svc = (System.Windows.Application.Current as App)?.SettingsService; if (svc == null) return; svc.Settings.Launcher.DockBarLeft = -1; svc.Settings.Launcher.DockBarTop = -1; svc.Save(); // 즉시 독 바 위치 이동 (System.Windows.Application.Current as App)?.RefreshDockBar(); } // ─── 저장 공간 관리 ────────────────────────────────────────────────────── private void RefreshStorageInfo() { if (StorageSummaryText == null) return; var report = StorageAnalyzer.Analyze(); StorageSummaryText.Text = $"앱 전체 사용량: {StorageAnalyzer.FormatSize(report.TotalAppUsage)}"; StorageDriveText.Text = $"드라이브 {report.DriveLabel} 여유: {StorageAnalyzer.FormatSize(report.DriveFreeSpace)} / {StorageAnalyzer.FormatSize(report.DriveTotalSpace)}"; if (StorageDetailPanel == null) return; StorageDetailPanel.Children.Clear(); var items = new (string Label, long Size)[] { ("대화 기록", report.Conversations), ("감사 로그", report.AuditLogs), ("앱 로그", report.Logs), ("코드 인덱스", report.CodeIndex), ("임베딩 DB", report.EmbeddingDb), ("클립보드 히스토리", report.ClipboardHistory), ("플러그인", report.Plugins), ("JSON 스킬", report.Skills), }; foreach (var (label, size) in items) { if (size == 0) continue; var row = new Grid { Margin = new Thickness(0, 2, 0, 2) }; row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var labelTb = new TextBlock { Text = label, FontSize = 12, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, VerticalAlignment = VerticalAlignment.Center }; Grid.SetColumn(labelTb, 0); row.Children.Add(labelTb); var sizeTb = new TextBlock { Text = StorageAnalyzer.FormatSize(size), FontSize = 12, FontFamily = new FontFamily("Consolas"), Foreground = Brushes.Gray, VerticalAlignment = VerticalAlignment.Center }; Grid.SetColumn(sizeTb, 1); row.Children.Add(sizeTb); StorageDetailPanel.Children.Add(row); } } private void BtnStorageRefresh_Click(object sender, RoutedEventArgs e) => RefreshStorageInfo(); private void RefreshStorageInfo2() { if (StorageSummaryText2 == null) return; var report = StorageAnalyzer.Analyze(); StorageSummaryText2.Text = $"앱 전체 사용량: {StorageAnalyzer.FormatSize(report.TotalAppUsage)}"; StorageDriveText2.Text = $"드라이브 {report.DriveLabel} 여유: {StorageAnalyzer.FormatSize(report.DriveFreeSpace)} / {StorageAnalyzer.FormatSize(report.DriveTotalSpace)}"; if (StorageDetailPanel2 == null) return; StorageDetailPanel2.Children.Clear(); var items = new (string Label, long Size)[] { ("대화 기록", report.Conversations), ("감사 로그", report.AuditLogs), ("앱 로그", report.Logs), ("코드 인덱스", report.CodeIndex), ("임베딩 DB", report.EmbeddingDb), ("클립보드 히스토리", report.ClipboardHistory), ("플러그인", report.Plugins), ("JSON 스킬", report.Skills), }; foreach (var (label, size) in items) { if (size == 0) continue; var row = new Grid { Margin = new Thickness(0, 2, 0, 2) }; row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var labelTb = new TextBlock { Text = label, FontSize = 12, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black }; Grid.SetColumn(labelTb, 0); row.Children.Add(labelTb); var sizeTb = new TextBlock { Text = StorageAnalyzer.FormatSize(size), FontSize = 12, FontFamily = new FontFamily("Consolas"), Foreground = Brushes.Gray }; Grid.SetColumn(sizeTb, 1); row.Children.Add(sizeTb); StorageDetailPanel2.Children.Add(row); } } private void BtnStorageRefresh2_Click(object sender, RoutedEventArgs e) => RefreshStorageInfo2(); private void BtnStorageCleanup_Click(object sender, RoutedEventArgs e) { // 테마 리소스 조회 var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E)); var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF)); var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x22, 0xFF, 0xFF, 0xFF)); var shadowColor = TryFindResource("ShadowColor") is Color sc ? sc : Colors.Black; // 보관 기간 선택 팝업 ? 커스텀 버튼으로 날짜 선택 var popup = new Window { WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent, Width = 360, SizeToContent = SizeToContent.Height, WindowStartupLocation = WindowStartupLocation.CenterOwner, Owner = this, ShowInTaskbar = false, Topmost = true, }; int selectedDays = -1; var outerBorder = new Border { Background = bgBrush, CornerRadius = new CornerRadius(14), BorderBrush = borderBrush, BorderThickness = new Thickness(1), Margin = new Thickness(16), Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 20, ShadowDepth = 4, Opacity = 0.3, Color = shadowColor }, }; var stack = new StackPanel { Margin = new Thickness(24, 20, 24, 20) }; stack.Children.Add(new TextBlock { Text = "보관 기간 선택", FontSize = 15, FontWeight = FontWeights.SemiBold, Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 6) }); stack.Children.Add(new TextBlock { Text = "선택한 기간 이전의 데이터를 삭제합니다.\n※ 통계/대화 기록은 삭제되지 않습니다.", FontSize = 12, Foreground = subFgBrush, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 0, 0, 16) }); var btnDays = new (int Days, string Label)[] { (7, "최근 7일만 보관"), (14, "최근 14일만 보관"), (30, "최근 30일만 보관"), (0, "전체 삭제") }; foreach (var (days, label) in btnDays) { var d = days; var isDelete = d == 0; var deleteBg = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0x44, 0x44)); var deleteBorder = new SolidColorBrush(Color.FromArgb(0x40, 0xFF, 0x44, 0x44)); var deleteText = new SolidColorBrush(Color.FromRgb(0xFF, 0x66, 0x66)); var btn = new Border { CornerRadius = new CornerRadius(10), Cursor = Cursors.Hand, Padding = new Thickness(14, 10, 14, 10), Margin = new Thickness(0, 0, 0, 6), Background = isDelete ? deleteBg : itemBg, BorderBrush = isDelete ? deleteBorder : borderBrush, BorderThickness = new Thickness(1), }; btn.Child = new TextBlock { Text = label, FontSize = 13, Foreground = isDelete ? deleteText : fgBrush }; var normalBg = isDelete ? deleteBg : itemBg; btn.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; }; btn.MouseLeave += (s, _) => { if (s is Border b) b.Background = normalBg; }; btn.MouseLeftButtonUp += (_, _) => { selectedDays = d; popup.Close(); }; stack.Children.Add(btn); } // 취소 var cancelBtn = new Border { CornerRadius = new CornerRadius(10), Cursor = Cursors.Hand, Padding = new Thickness(14, 8, 14, 8), Margin = new Thickness(0, 6, 0, 0), Background = Brushes.Transparent }; cancelBtn.Child = new TextBlock { Text = "취소", FontSize = 12, Foreground = subFgBrush, HorizontalAlignment = HorizontalAlignment.Center }; cancelBtn.MouseLeftButtonUp += (_, _) => popup.Close(); stack.Children.Add(cancelBtn); outerBorder.Child = stack; popup.Content = outerBorder; popup.ShowDialog(); if (selectedDays < 0) return; // 삭제 전 확인 var confirmMsg = selectedDays == 0 ? "전체 데이터를 삭제합니다. 정말 진행하시겠습니까?" : $"최근 {selectedDays}일 이전의 데이터를 삭제합니다. 정말 진행하시겠습니까?"; var confirm = CustomMessageBox.Show(confirmMsg, "삭제 확인", MessageBoxButton.YesNo, MessageBoxImage.Warning); if (confirm != MessageBoxResult.Yes) return; var freed = StorageAnalyzer.Cleanup( retainDays: selectedDays, cleanConversations: false, cleanAuditLogs: true, cleanLogs: true, cleanCodeIndex: true, cleanClipboard: selectedDays == 0 ); CustomMessageBox.Show( $"{StorageAnalyzer.FormatSize(freed)}를 확보했습니다.", "정리 완료", MessageBoxButton.OK, MessageBoxImage.Information); RefreshStorageInfo(); } // ─── 알림 카테고리 체크박스 ─────────────────────────────────────────────── private void BuildQuoteCategoryCheckboxes() { if (QuoteCategoryPanel == null) return; QuoteCategoryPanel.Children.Clear(); var enabled = _vm.GetReminderCategories(); var toggleStyle = TryFindResource("ToggleSwitch") as Style; foreach (var (key, label, countFunc) in Services.QuoteService.Categories) { var count = countFunc(); var displayLabel = key == "today_events" ? $"{label} ({count}개, 오늘 {Services.QuoteService.GetTodayMatchCount()}개)" : $"{label} ({count}개)"; // 좌: 라벨, 우: 토글 스위치 var row = new Grid { Margin = new Thickness(0, 3, 0, 3) }; row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var labelBlock = new TextBlock { Text = displayLabel, FontSize = 12.5, VerticalAlignment = VerticalAlignment.Center, Foreground = TryFindResource("PrimaryText") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.White, }; Grid.SetColumn(labelBlock, 0); row.Children.Add(labelBlock); var cb = new CheckBox { IsChecked = enabled.Contains(key, StringComparer.OrdinalIgnoreCase), Tag = key, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center, }; if (toggleStyle != null) cb.Style = toggleStyle; cb.Checked += (s, _) => { if (s is CheckBox c && c.Tag is string k && !enabled.Contains(k)) enabled.Add(k); }; cb.Unchecked += (s, _) => { if (s is CheckBox c && c.Tag is string k) enabled.RemoveAll(x => x.Equals(k, StringComparison.OrdinalIgnoreCase)); }; Grid.SetColumn(cb, 1); row.Children.Add(cb); QuoteCategoryPanel.Children.Add(row); } } // ─── 버전 표시 ────────────────────────────────────────────────────────── /// /// 하단 버전 텍스트를 AxCopilot.csproj <Version> 값에서 동적으로 읽어 설정합니다. /// 버전을 올릴 때는 AxCopilot.csproj → <Version> 하나만 수정하면 됩니다. /// 이 함수와 SettingsWindow.xaml 의 VersionInfoText 는 항상 함께 유지됩니다. /// private void SetVersionText() { try { var asm = System.Reflection.Assembly.GetExecutingAssembly(); // FileVersionInfo 에서 읽어야 csproj 이 반영됩니다. var fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(asm.Location); var ver = fvi.ProductVersion ?? fvi.FileVersion ?? "?"; // 빌드 메타데이터 제거 (예: "1.0.3+gitabcdef" → "1.0.3") var plusIdx = ver.IndexOf('+'); if (plusIdx > 0) ver = ver[..plusIdx]; VersionInfoText.Text = $"AX Copilot · v{ver}"; } catch { VersionInfoText.Text = "AX Copilot"; } } // ─── 핫키 (콤보박스 선택 방식) ────────────────────────────────────────── /// 이전 녹화기에서 호출되던 초기화 ? 콤보박스 전환 후 무연산 (호환용) private void RefreshHotkeyBadges() { /* 콤보박스 SelectedValue 바인딩으로 대체 */ } /// 현재 핫키가 콤보박스 목록에 없으면 항목으로 추가합니다. private void EnsureHotkeyInCombo() { if (HotkeyCombo == null) return; var hotkey = _vm.Hotkey; if (string.IsNullOrWhiteSpace(hotkey)) return; // 이미 목록에 있는지 확인 foreach (System.Windows.Controls.ComboBoxItem item in HotkeyCombo.Items) { if (item.Tag is string tag && tag == hotkey) return; } // 목록에 없으면 현재 값을 추가 var display = hotkey.Replace("+", " + "); var newItem = new System.Windows.Controls.ComboBoxItem { Content = $"{display} (사용자 정의)", Tag = hotkey }; HotkeyCombo.Items.Insert(0, newItem); HotkeyCombo.SelectedIndex = 0; } /// Window-level PreviewKeyDown ? 핫키 녹화 제거 후 잔여 호출 보호 private void Window_PreviewKeyDown(object sender, KeyEventArgs e) { } /// WPF Key → HotkeyParser가 인식하는 문자열 이름. private static string GetKeyName(Key key) => key switch { Key.Space => "Space", Key.Enter or Key.Return => "Enter", Key.Tab => "Tab", Key.Back => "Backspace", Key.Delete => "Delete", Key.Escape => "Escape", Key.Home => "Home", Key.End => "End", Key.PageUp => "PageUp", Key.PageDown => "PageDown", Key.Left => "Left", Key.Right => "Right", Key.Up => "Up", Key.Down => "Down", Key.Insert => "Insert", // A?Z >= Key.A and <= Key.Z => key.ToString(), // 0?9 (메인 키보드) >= Key.D0 and <= Key.D9 => ((int)(key - Key.D0)).ToString(), // F1?F12 >= Key.F1 and <= Key.F12 => key.ToString(), // 기호 Key.OemTilde => "`", Key.OemMinus => "-", Key.OemPlus => "=", Key.OemOpenBrackets => "[", Key.OemCloseBrackets => "]", Key.OemPipe or Key.OemBackslash => "\\", Key.OemSemicolon => ";", Key.OemQuotes => "'", Key.OemComma => ",", Key.OemPeriod => ".", Key.OemQuestion => "/", _ => key.ToString() }; private void HotkeyCombo_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { // 콤보박스 선택이 바뀌면 ViewModel의 Hotkey를 업데이트 // (바인딩이 SelectedValue에 연결되어 자동 처리되지만, // 기존 RefreshHotkeyBadges 호출은 콤보박스 도입으로 불필요) } // ─── 기존 이벤트 핸들러 ────────────────────────────────────────────────── private async void BtnTestConnection_Click(object sender, RoutedEventArgs e) { var btn = sender as Button; if (btn != null) btn.Content = "테스트 중..."; try { // 현재 UI 값으로 임시 LLM 서비스 생성하여 테스트 (설정 저장/창 닫기 없이) var llm = new Services.LlmService(_vm.Service); var (ok, msg) = await llm.TestConnectionAsync(); llm.Dispose(); CustomMessageBox.Show(msg, ok ? "연결 성공" : "연결 실패", MessageBoxButton.OK, ok ? MessageBoxImage.Information : MessageBoxImage.Warning); } catch (Exception ex) { CustomMessageBox.Show(ex.Message, "오류", MessageBoxButton.OK, MessageBoxImage.Error); } finally { if (btn != null) btn.Content = "테스트"; } } // ─── 등록 모델 관리 ────────────────────────────────────────────────── private bool IsEncryptionEnabled => (Application.Current as App)?.SettingsService?.Settings.Llm.EncryptionEnabled ?? false; /// 현재 선택된 서비스 서브탭 이름을 반환합니다. private string GetCurrentServiceSubTab() { if (SvcTabOllama?.IsChecked == true) return "ollama"; if (SvcTabVllm?.IsChecked == true) return "vllm"; if (SvcTabGemini?.IsChecked == true) return "gemini"; if (SvcTabSigmoid?.IsChecked == true) return "claude"; return _vm.LlmService; // 폴백 } private void BtnBrowseSkillFolder_Click(object sender, RoutedEventArgs e) { var dlg = new System.Windows.Forms.FolderBrowserDialog { Description = "스킬 파일이 있는 폴더를 선택하세요", ShowNewFolderButton = true, }; if (!string.IsNullOrEmpty(_vm.SkillsFolderPath) && System.IO.Directory.Exists(_vm.SkillsFolderPath)) dlg.SelectedPath = _vm.SkillsFolderPath; if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) _vm.SkillsFolderPath = dlg.SelectedPath; } private void BtnOpenSkillFolder_Click(object sender, RoutedEventArgs e) { // 기본 스킬 폴더 열기 (사용자 앱데이터) var folder = !string.IsNullOrEmpty(_vm.SkillsFolderPath) && System.IO.Directory.Exists(_vm.SkillsFolderPath) ? _vm.SkillsFolderPath : System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills"); if (!System.IO.Directory.Exists(folder)) System.IO.Directory.CreateDirectory(folder); try { System.Diagnostics.Process.Start("explorer.exe", folder); } catch { } } // ─── 스킬 가져오기/내보내기 ────────────────────────────────────────── private void SkillImport_Click(object sender, MouseButtonEventArgs e) { var dlg = new Microsoft.Win32.OpenFileDialog { Filter = "스킬 패키지 (*.zip)|*.zip", Title = "가져올 스킬 zip 파일을 선택하세요", }; if (dlg.ShowDialog() != true) return; var count = Services.Agent.SkillService.ImportSkills(dlg.FileName); if (count > 0) CustomMessageBox.Show($"스킬 {count}개를 성공적으로 가져왔습니??.\n스킬 목록이 갱신됩니다.", "스킬 가져오기"); else CustomMessageBox.Show("스킬 가져오기에 실패했습니다.\nzip 파일에 .skill.md 또는 SKILL.md 파일이 포함되어야 합니다.", "스킬 가져오기"); } private void SkillExport_Click(object sender, MouseButtonEventArgs e) { var skills = Services.Agent.SkillService.Skills; if (skills.Count == 0) { CustomMessageBox.Show("내보낼 스킬이 없습니다.", "스킬 내보내기"); return; } // 스킬 선택 팝업 var popup = new Window { WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent, SizeToContent = SizeToContent.WidthAndHeight, WindowStartupLocation = WindowStartupLocation.CenterOwner, Owner = this, MaxHeight = 450, }; var bgBrush = TryFindResource("LauncherBackground") as Brush ?? Brushes.White; var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.Black; var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var outer = new Border { Background = bgBrush, CornerRadius = new CornerRadius(12), Padding = new Thickness(20), MinWidth = 360, Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 20, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black, }, }; var mainPanel = new StackPanel(); mainPanel.Children.Add(new TextBlock { Text = "내보낼 스킬 선택", FontSize = 15, FontWeight = FontWeights.SemiBold, Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 12), }); var checkBoxes = new List<(CheckBox cb, Services.Agent.SkillDefinition skill)>(); var listPanel = new StackPanel(); foreach (var skill in skills) { var cb = new CheckBox { Content = $"/{skill.Name} ? {skill.Label}", FontSize = 12.5, Foreground = fgBrush, Margin = new Thickness(0, 3, 0, 3), IsChecked = false, }; checkBoxes.Add((cb, skill)); listPanel.Children.Add(cb); } var scrollViewer = new ScrollViewer { Content = listPanel, MaxHeight = 280, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, }; mainPanel.Children.Add(scrollViewer); // 버튼 행 var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 14, 0, 0), }; var cancelBtn = new Border { Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F0F1F5")), CornerRadius = new CornerRadius(8), Padding = new Thickness(16, 7, 16, 7), Margin = new Thickness(0, 0, 8, 0), Cursor = Cursors.Hand, Child = new TextBlock { Text = "취소", FontSize = 12, Foreground = subFgBrush }, }; cancelBtn.MouseLeftButtonUp += (_, _) => popup.Close(); btnRow.Children.Add(cancelBtn); var okBtn = new Border { Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5EFC")), CornerRadius = new CornerRadius(8), Padding = new Thickness(16, 7, 16, 7), Cursor = Cursors.Hand, Child = new TextBlock { Text = "내보내기", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White }, }; okBtn.MouseLeftButtonUp += (_, _) => { var selected = checkBoxes.Where(x => x.cb.IsChecked == true).Select(x => x.skill).ToList(); if (selected.Count == 0) { CustomMessageBox.Show("내보낼 스킬을 선택하세요.", "스킬 내보내기"); return; } // 저장 폴더 선택 var folderDlg = new System.Windows.Forms.FolderBrowserDialog { Description = "스킬 zip을 저장할 폴더를 선택하세요", }; if (folderDlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return; int exported = 0; foreach (var skill in selected) { var result = Services.Agent.SkillService.ExportSkill(skill, folderDlg.SelectedPath); if (result != null) exported++; } popup.Close(); if (exported > 0) CustomMessageBox.Show($"{exported}개 스킬을 내보냈습니다.\n경로: {folderDlg.SelectedPath}", "스킬 내보내기"); else CustomMessageBox.Show("내보내기에 실패했습니다.", "스킬 내보내기"); }; btnRow.Children.Add(okBtn); mainPanel.Children.Add(btnRow); outer.Child = mainPanel; popup.Content = outer; popup.KeyDown += (_, ke) => { if (ke.Key == Key.Escape) popup.Close(); }; popup.ShowDialog(); } private void BtnAddModel_Click(object sender, RoutedEventArgs e) { var currentService = GetCurrentServiceSubTab(); var dlg = new ModelRegistrationDialog(currentService); dlg.Owner = this; if (dlg.ShowDialog() == true) { _vm.RegisteredModels.Add(new ViewModels.RegisteredModelRow { Alias = dlg.ModelAlias, EncryptedModelName = Services.CryptoService.EncryptIfEnabled(dlg.ModelName, IsEncryptionEnabled), Service = currentService, Endpoint = dlg.Endpoint, ApiKey = dlg.ApiKey, AllowInsecureTls = dlg.AllowInsecureTls, AuthType = dlg.AuthType, Cp4dUrl = dlg.Cp4dUrl, Cp4dUsername = dlg.Cp4dUsername, Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsEncryptionEnabled), }); BuildFallbackModelsPanel(); BuildServiceModelPanels(); } } private void BtnEditModel_Click(object sender, RoutedEventArgs e) { if (sender is not Button btn || btn.Tag is not ViewModels.RegisteredModelRow row) return; string currentModel = Services.CryptoService.DecryptIfEnabled(row.EncryptedModelName, IsEncryptionEnabled); string cp4dPw = Services.CryptoService.DecryptIfEnabled(row.Cp4dPassword ?? "", IsEncryptionEnabled); var currentService = GetCurrentServiceSubTab(); var dlg = new ModelRegistrationDialog(currentService, row.Alias, currentModel, row.Endpoint, row.ApiKey, row.AllowInsecureTls, row.AuthType ?? "bearer", row.Cp4dUrl ?? "", row.Cp4dUsername ?? "", cp4dPw); dlg.Owner = this; if (dlg.ShowDialog() == true) { row.Alias = dlg.ModelAlias; row.EncryptedModelName = Services.CryptoService.EncryptIfEnabled(dlg.ModelName, IsEncryptionEnabled); row.Service = currentService; row.Endpoint = dlg.Endpoint; row.ApiKey = dlg.ApiKey; row.AllowInsecureTls = dlg.AllowInsecureTls; row.AuthType = dlg.AuthType; row.Cp4dUrl = dlg.Cp4dUrl; row.Cp4dUsername = dlg.Cp4dUsername; row.Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsEncryptionEnabled); BuildFallbackModelsPanel(); BuildServiceModelPanels(); } } private void BtnDeleteModel_Click(object sender, RoutedEventArgs e) { if (sender is not Button btn || btn.Tag is not ViewModels.RegisteredModelRow row) return; var result = CustomMessageBox.Show($"'{row.Alias}' 모델을 삭제하시겠습니까?", "모델 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result == MessageBoxResult.Yes) { _vm.RegisteredModels.Remove(row); BuildFallbackModelsPanel(); BuildServiceModelPanels(); } } // ─── 프롬프트 템플릿 관리 ──────────────────────────────────────────── private void BtnAddTemplate_Click(object sender, RoutedEventArgs e) { var dlg = new PromptTemplateDialog(); dlg.Owner = this; if (dlg.ShowDialog() == true) { _vm.PromptTemplates.Add(new ViewModels.PromptTemplateRow { Name = dlg.TemplateName, Content = dlg.TemplateContent, }); } } private void BtnEditTemplate_Click(object sender, RoutedEventArgs e) { if (sender is not Button btn || btn.Tag is not ViewModels.PromptTemplateRow row) return; var dlg = new PromptTemplateDialog(row.Name, row.Content); dlg.Owner = this; if (dlg.ShowDialog() == true) { row.Name = dlg.TemplateName; row.Content = dlg.TemplateContent; } } private void BtnDeleteTemplate_Click(object sender, RoutedEventArgs e) { if (sender is not Button btn || btn.Tag is not ViewModels.PromptTemplateRow row) return; var result = CustomMessageBox.Show($"'{row.Name}' 템플릿을 삭제하시겠습니까?", "템플릿 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result == MessageBoxResult.Yes) _vm.PromptTemplates.Remove(row); } // ─── AX Agent 서브탭 전환 ─────────────────────────────────────────── private void AgentSubTab_Checked(object sender, RoutedEventArgs e) { if (AgentPanelCommon == null) return; // 초기화 전 방어 var showCommon = AgentTabCommon.IsChecked == true; var showService = AgentTabChat.IsChecked == true; var showPermission = AgentTabCoworkCode.IsChecked == true; var showAdvanced = AgentTabDev.IsChecked == true; AgentPanelCommon.Visibility = showCommon || showService ? Visibility.Visible : Visibility.Collapsed; if (AgentCommonOverviewSection != null) AgentCommonOverviewSection.Visibility = showCommon ? Visibility.Visible : Visibility.Collapsed; if (AgentCommonRuntimeSection != null) AgentCommonRuntimeSection.Visibility = showCommon ? Visibility.Visible : Visibility.Collapsed; if (AgentServiceSection != null) AgentServiceSection.Visibility = showService ? Visibility.Visible : Visibility.Collapsed; AgentPanelCoworkCode.Visibility = showPermission ? Visibility.Visible : Visibility.Collapsed; AgentPanelChat.Visibility = Visibility.Collapsed; AgentPanelCowork.Visibility = Visibility.Collapsed; AgentPanelCode.Visibility = Visibility.Collapsed; if (AgentPanelDev != null) AgentPanelDev.Visibility = showAdvanced ? Visibility.Visible : Visibility.Collapsed; if (AgentPanelEtc != null) AgentPanelEtc.Visibility = Visibility.Collapsed; if (AgentPanelTools != null) AgentPanelTools.Visibility = Visibility.Collapsed; } // ─── 도구 관리 카드 UI ────────────────────────────────────────────── private bool _toolCardsLoaded; private HashSet _disabledTools = new(StringComparer.OrdinalIgnoreCase); /// 도구 카드 UI를 카테고리별로 생성합니다. private void LoadToolCards() { if (_toolCardsLoaded || ToolCardsPanel == null) return; _toolCardsLoaded = true; var app = System.Windows.Application.Current as App; var settings = app?.SettingsService?.Settings.Llm; using var tools = Services.Agent.ToolRegistry.CreateDefault(); _disabledTools = new HashSet(settings?.DisabledTools ?? new(), StringComparer.OrdinalIgnoreCase); var disabled = _disabledTools; // 카테고리 매핑 var categories = new Dictionary> { ["파일/검색"] = new(), ["문서 생성"] = new(), ["문서 품질"] = new(), ["코드/개발"] = new(), ["데이터/유틸"] = new(), ["시스템"] = new(), }; var toolCategoryMap = new Dictionary(StringComparer.OrdinalIgnoreCase) { // 파일/검색 ["file_read"] = "파일/검색", ["file_write"] = "파일/검색", ["file_edit"] = "파일/검색", ["glob"] = "파일/검색", ["grep"] = "파일/검색", ["folder_map"] = "파일/검색", ["document_read"] = "파일/검색", ["file_watch"] = "파일/검색", // 문서 생성 ["excel_skill"] = "문서 생성", ["docx_skill"] = "문서 생성", ["csv_skill"] = "문서 생성", ["markdown_skill"] = "문서 생성", ["html_skill"] = "문서 생성", ["chart_skill"] = "문서 생성", ["batch_skill"] = "문서 생성", ["pptx_skill"] = "문서 생성", ["document_planner"] = "문서 생성", ["document_assembler"] = "문서 생성", // 문서 품질 ["document_review"] = "문서 품질", ["format_convert"] = "문서 품질", ["template_render"] = "문서 품질", ["text_summarize"] = "문서 품질", // 코드/개발 ["dev_env_detect"] = "코드/개발", ["build_run"] = "코드/개발", ["git_tool"] = "코드/개발", ["lsp"] = "코드/개발", ["sub_agent"] = "코드/개발", ["wait_agents"] = "코드/개발", ["code_search"] = "코드/개발", ["test_loop"] = "코드/개발", ["code_review"] = "코드/개발", ["project_rule"] = "코드/개발", // 시스템 ["process"] = "시스템", ["skill_manager"] = "시스템", ["memory"] = "시스템", ["clipboard"] = "시스템", ["notify"] = "시스템", ["env"] = "시스템", ["image_analyze"] = "시스템", }; foreach (var tool in tools.All) { var cat = toolCategoryMap.TryGetValue(tool.Name, out var c) ? c : "데이터/유틸"; if (categories.ContainsKey(cat)) categories[cat].Add(tool); else categories["데이터/유틸"].Add(tool); } var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0xF5, 0xF5, 0xF8)); foreach (var kv in categories) { if (kv.Value.Count == 0) continue; // 카테고리 헤더 var header = new TextBlock { Text = $"{kv.Key} ({kv.Value.Count})", FontSize = 13, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, Margin = new Thickness(0, 8, 0, 6), }; ToolCardsPanel.Children.Add(header); // 카드 WrapPanel var wrap = new WrapPanel { Margin = new Thickness(0, 0, 0, 4) }; foreach (var tool in kv.Value.OrderBy(t => t.Name)) { var isEnabled = !disabled.Contains(tool.Name); var card = CreateToolCard(tool, isEnabled, disabled, accentBrush, secondaryText, itemBg); wrap.Children.Add(card); } ToolCardsPanel.Children.Add(wrap); } // MCP 서버 상태 LoadMcpStatus(); } /// 개별 도구 카드를 생성합니다 (이름 + 설명 + 토글). private Border CreateToolCard(Services.Agent.IAgentTool tool, bool isEnabled, HashSet disabled, Brush accentBrush, Brush secondaryText, Brush itemBg) { var card = new Border { Background = itemBg, CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 8, 10, 8), Margin = new Thickness(0, 0, 8, 8), Width = 240, BorderBrush = isEnabled ? Brushes.Transparent : new SolidColorBrush(Color.FromArgb(0x30, 0xDC, 0x26, 0x26)), BorderThickness = new Thickness(1), }; var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // 이름 + 설명 var textStack = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; var nameBlock = new TextBlock { Text = tool.Name, FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, TextTrimming = TextTrimming.CharacterEllipsis, }; textStack.Children.Add(nameBlock); var desc = tool.Description; if (desc.Length > 50) desc = desc[..50] + "…"; var descBlock = new TextBlock { Text = desc, FontSize = 10.5, Foreground = secondaryText, TextTrimming = TextTrimming.CharacterEllipsis, Margin = new Thickness(0, 2, 0, 0), }; textStack.Children.Add(descBlock); Grid.SetColumn(textStack, 0); grid.Children.Add(textStack); // 토글 (CheckBox + ToggleSwitch 스타일) var toggle = new CheckBox { IsChecked = isEnabled, Style = TryFindResource("ToggleSwitch") as Style, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(8, 0, 0, 0), }; toggle.Checked += (_, _) => { disabled.Remove(tool.Name); card.BorderBrush = Brushes.Transparent; }; toggle.Unchecked += (_, _) => { disabled.Add(tool.Name); card.BorderBrush = new SolidColorBrush(Color.FromArgb(0x30, 0xDC, 0x26, 0x26)); }; Grid.SetColumn(toggle, 1); grid.Children.Add(toggle); card.Child = grid; return card; } /// MCP 서버 연결 상태 표시를 생성합니다. private void LoadMcpStatus() { if (McpStatusPanel == null) return; McpStatusPanel.Children.Clear(); var app = System.Windows.Application.Current as App; var settings = app?.SettingsService?.Settings.Llm; var mcpServers = settings?.McpServers; if (mcpServers == null || mcpServers.Count == 0) { McpStatusPanel.Children.Add(new TextBlock { Text = "등록된 MCP 서버가 없습니다.", FontSize = 12, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, Margin = new Thickness(0, 4, 0, 0), }); return; } var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0xF5, 0xF5, 0xF8)); foreach (var server in mcpServers) { var row = new Border { Background = itemBg, CornerRadius = new CornerRadius(8), Padding = new Thickness(12, 8, 12, 8), Margin = new Thickness(0, 0, 0, 6), }; var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // 상태 아이콘 var statusDot = new Border { Width = 8, Height = 8, CornerRadius = new CornerRadius(4), Background = new SolidColorBrush(Color.FromRgb(0x34, 0xA8, 0x53)), // 녹색 (등록됨) VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), }; Grid.SetColumn(statusDot, 0); grid.Children.Add(statusDot); // 서버 이름 + 명령 var infoStack = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; infoStack.Children.Add(new TextBlock { Text = server.Name ?? "(이름 없음)", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, }); infoStack.Children.Add(new TextBlock { Text = server.Command ?? "", FontSize = 10.5, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, TextTrimming = TextTrimming.CharacterEllipsis, }); Grid.SetColumn(infoStack, 1); grid.Children.Add(infoStack); // 상태 텍스트 var statusText = new TextBlock { Text = "등록됨", FontSize = 11, Foreground = new SolidColorBrush(Color.FromRgb(0x34, 0xA8, 0x53)), VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(statusText, 2); grid.Children.Add(statusText); row.Child = grid; McpStatusPanel.Children.Add(row); } } // ─── 기능 설정 서브탭 전환 ────────────────────────────────────────── private void FuncSubTab_Checked(object sender, RoutedEventArgs e) { if (FuncPanel_AI == null) return; // 초기화 전 방어 FuncPanel_AI.Visibility = FuncSubTab_AI.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; FuncPanel_Launcher.Visibility = FuncSubTab_Launcher.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; FuncPanel_Design.Visibility = FuncSubTab_Design.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; } // ─── 접기/열기 섹션 토글 ─────────────────────────────────────────── private void CollapsibleSection_Toggle(object sender, System.Windows.Input.MouseButtonEventArgs e) { if (sender is Border border && border.Tag is System.Windows.Controls.Expander expander) expander.IsExpanded = !expander.IsExpanded; } private void ServiceSubTab_Checked(object sender, RoutedEventArgs e) { if (SvcPanelOllama == null) return; // 초기화 전 방어 SvcPanelOllama.Visibility = SvcTabOllama.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; SvcPanelVllm.Visibility = SvcTabVllm.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; SvcPanelGemini.Visibility = SvcTabGemini.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; SvcPanelSigmoid.Visibility = SvcTabSigmoid.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; BuildServiceModelPanels(); SyncAgentSelectionCards(); } private void AgentServiceCard_Checked(object sender, RoutedEventArgs e) { if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true) return; var service = rb.Name switch { "AgentServiceCardVllm" => "vllm", "AgentServiceCardGemini" => "gemini", "AgentServiceCardClaude" => "claude", _ => "ollama", }; _vm.LlmService = service; if (SvcTabOllama != null) SvcTabOllama.IsChecked = service == "ollama"; if (SvcTabVllm != null) SvcTabVllm.IsChecked = service == "vllm"; if (SvcTabGemini != null) SvcTabGemini.IsChecked = service == "gemini"; if (SvcTabSigmoid != null) SvcTabSigmoid.IsChecked = service == "claude"; BuildServiceModelPanels(); } private void AgentPermissionCard_Checked(object sender, RoutedEventArgs e) { if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true) return; _vm.DefaultAgentPermission = rb.Name switch { "AgentPermissionCardPlan" => "Plan", "AgentPermissionCardAuto" => "Auto", "AgentPermissionCardDeny" => "Deny", _ => "Ask", }; } private void AgentDecisionCard_Checked(object sender, RoutedEventArgs e) { if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true) return; _vm.AgentDecisionLevel = rb.Name switch { "AgentDecisionCardMinimal" => "minimal", "AgentDecisionCardDetailed" => "detailed", _ => "normal", }; } private void AgentOperationModeCard_Checked(object sender, RoutedEventArgs e) { if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true) return; var next = rb.Name == "AgentOperationModeExternal" ? OperationModePolicy.ExternalMode : OperationModePolicy.InternalMode; var app = System.Windows.Application.Current as App; var settings = app?.SettingsService?.Settings; if (settings == null) return; var current = OperationModePolicy.Normalize(settings.OperationMode); if (string.Equals(next, current, StringComparison.OrdinalIgnoreCase)) return; var ok = PromptPasswordDialog( "운영 모드 변경 ? 비밀번호 확인", "\U0001f512 사내/사외 모드 변경", "비밀번호를 입력하세요:"); if (!ok) { ApplyOperationModeState(settings.OperationMode); return; } settings.OperationMode = next; _vm.OperationMode = next; app?.SettingsService?.Save(); ApplyOperationModeState(next); } private void AgentContextTokensCard_Checked(object sender, RoutedEventArgs e) { if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true) return; _vm.LlmMaxContextTokens = rb.Name switch { "AgentContextTokens16K" => 16384, "AgentContextTokens64K" => 65536, "AgentContextTokens256K" => 262144, "AgentContextTokens1M" => 1_000_000, _ => 4096, }; } private void AgentRetentionDaysCard_Checked(object sender, RoutedEventArgs e) { if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true) return; _vm.LlmRetentionDays = rb.Name switch { "AgentRetentionDays30" => 30, "AgentRetentionDays90" => 90, "AgentRetentionDaysUnlimited" => 0, _ => 7, }; } private void AgentLogLevelCard_Checked(object sender, RoutedEventArgs e) { if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true) return; _vm.AgentLogLevel = rb.Name switch { "AgentLogLevelDetailed" => "detailed", "AgentLogLevelDebug" => "debug", _ => "simple", }; } private void BuildServiceModelPanels() { BuildRegisteredModelList("ollama", OllamaRegisteredModelsList); BuildRegisteredModelList("vllm", VllmRegisteredModelsList); BuildServiceModelChips("ollama", OllamaModelChipPanel, _vm.OllamaModel, value => _vm.OllamaModel = value); BuildServiceModelChips("vllm", VllmModelChipPanel, _vm.VllmModel, value => _vm.VllmModel = value); BuildPresetModelChips( GeminiModelChipPanel, _vm.GeminiModel, value => _vm.GeminiModel = value, new (string Id, string Label, string Hint)[] { ("gemini-3.1-pro-preview", "Gemini 3.1 Pro", "최고 성능"), ("gemini-3-flash", "Gemini 3 Flash", "빠른 응답"), ("gemini-3.1-flash-lite-preview", "Gemini 3.1 Flash Lite", "경량"), ("gemini-2.5-flash", "Gemini 2.5 Flash", "균형"), ("gemini-2.5-flash-lite", "Gemini 2.5 Flash Lite", "저비용"), }); BuildPresetModelChips( ClaudeModelChipPanel, _vm.ClaudeModel, value => _vm.ClaudeModel = value, new (string Id, string Label, string Hint)[] { (ProviderModelIds.SigmoidOpus46, "Claude Opus 4.6", "최고 성능"), (ProviderModelIds.SigmoidSonnet46, "Claude Sonnet 4.6", "균형"), (ProviderModelIds.SigmoidHaiku45_20251001, "Claude Haiku 4.5", "경량"), (ProviderModelIds.SigmoidSonnet45_20250929, "Claude Sonnet 4.5", "이전 안정판"), (ProviderModelIds.SigmoidOpus4_20250514, "Claude Opus 4", "이전 최고급"), }); } private void BuildRegisteredModelList(string service, ItemsControl? target) { if (target == null) return; var view = new ListCollectionView(_vm.RegisteredModels); view.Filter = item => { if (item is not RegisteredModelRow row) return false; return string.Equals(row.Service, service, StringComparison.OrdinalIgnoreCase); }; target.ItemsSource = view; } private void BuildServiceModelChips(string service, WrapPanel? panel, string selectedValue, Action applySelection) { if (panel == null) return; panel.Children.Clear(); var models = _vm.RegisteredModels .Where(row => string.Equals(row.Service, service, StringComparison.OrdinalIgnoreCase)) .ToList(); if (models.Count == 0) { panel.Children.Add(new Border { Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke, BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(10), Padding = new Thickness(12, 8, 12, 8), Child = new TextBlock { Text = "등록된 모델이 없습니다. + 모델 추가로 먼저 등록하세요.", FontSize = 11.5, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, } }); return; } foreach (var model in models) { var isSelected = string.Equals(model.EncryptedModelName, selectedValue, StringComparison.OrdinalIgnoreCase); var chip = new Border { Cursor = Cursors.Hand, CornerRadius = new CornerRadius(10), BorderThickness = new Thickness(1), BorderBrush = isSelected ? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue) : (TryFindResource("BorderColor") as Brush ?? Brushes.LightGray), Background = isSelected ? (TryFindResource("ItemHoverBackground") as Brush ?? Brushes.AliceBlue) : Brushes.Transparent, Padding = new Thickness(12, 9, 12, 9), Margin = new Thickness(0, 0, 8, 8), Child = new StackPanel { Children = { new TextBlock { Text = string.IsNullOrWhiteSpace(model.Alias) ? "(이름 없음)" : model.Alias, FontSize = 12, FontWeight = isSelected ? FontWeights.SemiBold : FontWeights.Normal, Foreground = isSelected ? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue) : (TryFindResource("PrimaryText") as Brush ?? Brushes.Black), }, new TextBlock { Text = string.IsNullOrWhiteSpace(model.Endpoint) ? "기본 서버 사용" : model.Endpoint, FontSize = 10.5, Margin = new Thickness(0, 3, 0, 0), Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, } } } }; var captured = model.EncryptedModelName; chip.MouseLeftButtonUp += (_, _) => { applySelection(captured); BuildServiceModelPanels(); }; panel.Children.Add(chip); } } private void BuildPresetModelChips( WrapPanel? panel, string? selectedValue, Action applySelection, IEnumerable<(string Id, string Label, string Hint)> models) { if (panel == null) return; panel.Children.Clear(); foreach (var model in models) { var isSelected = string.Equals(model.Id, selectedValue, StringComparison.OrdinalIgnoreCase); var chip = new Border { Cursor = Cursors.Hand, CornerRadius = new CornerRadius(12), BorderThickness = new Thickness(1), BorderBrush = isSelected ? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue) : (TryFindResource("BorderColor") as Brush ?? Brushes.LightGray), Background = isSelected ? (TryFindResource("ItemHoverBackground") as Brush ?? Brushes.AliceBlue) : Brushes.Transparent, Padding = new Thickness(12, 9, 12, 9), Margin = new Thickness(0, 0, 8, 8), Child = new StackPanel { Children = { new TextBlock { Text = model.Label, FontSize = 12, FontWeight = isSelected ? FontWeights.SemiBold : FontWeights.Normal, Foreground = isSelected ? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue) : (TryFindResource("PrimaryText") as Brush ?? Brushes.Black), }, new TextBlock { Text = model.Hint, FontSize = 10.5, Margin = new Thickness(0, 3, 0, 0), Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, } } } }; var capturedId = model.Id; chip.MouseLeftButtonUp += (_, _) => { applySelection(capturedId); BuildServiceModelPanels(); }; panel.Children.Add(chip); } } private void ThemeCard_Click(object sender, RoutedEventArgs e) { if (sender is Button btn && btn.Tag is string key) _vm.SelectTheme(key); } private void ColorSwatch_Click(object sender, RoutedEventArgs e) { if (sender is Button btn && btn.Tag is ColorRowModel row) _vm.PickColor(row); } private void DevModeCheckBox_Checked(object sender, RoutedEventArgs e) { if (sender is not CheckBox cb || !cb.IsChecked.GetValueOrDefault()) return; // 설정 창 로드 중 바인딩에 의한 자동 Checked 이벤트 무시 (이미 활성화된 상태 복원) if (!IsLoaded) return; // 테마 리소스 조회 var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60)); var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40)); // 비밀번호 확인 다이얼로그 var dlg = new Window { Title = "개발자 모드 ? 비밀번호 확인", Width = 340, SizeToContent = SizeToContent.Height, WindowStartupLocation = WindowStartupLocation.CenterOwner, Owner = this, ResizeMode = ResizeMode.NoResize, WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent, }; var border = new Border { Background = bgBrush, CornerRadius = new CornerRadius(12), BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(20), }; var stack = new StackPanel(); stack.Children.Add(new TextBlock { Text = "\U0001f512 개발자 모드 활성화", FontSize = 15, FontWeight = FontWeights.SemiBold, Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 12), }); stack.Children.Add(new TextBlock { Text = "비밀번호를 입력하세요:", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 6), }); var pwBox = new PasswordBox { FontSize = 14, Padding = new Thickness(8, 6, 8, 6), Background = itemBg, Foreground = fgBrush, BorderBrush = borderBrush, PasswordChar = '*', }; stack.Children.Add(pwBox); var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) }; var cancelBtn = new Button { Content = "취소", Padding = new Thickness(16, 6, 16, 6), Margin = new Thickness(0, 0, 8, 0) }; cancelBtn.Click += (_, _) => { dlg.DialogResult = false; }; btnRow.Children.Add(cancelBtn); var okBtn = new Button { Content = "확인", Padding = new Thickness(16, 6, 16, 6), IsDefault = true }; okBtn.Click += (_, _) => { if (pwBox.Password == UnifiedAdminPassword) dlg.DialogResult = true; else { pwBox.Clear(); pwBox.Focus(); } }; btnRow.Children.Add(okBtn); stack.Children.Add(btnRow); border.Child = stack; dlg.Content = border; dlg.Loaded += (_, _) => pwBox.Focus(); if (dlg.ShowDialog() != true) { // 비밀번호 실패/취소 ? 체크 해제 + DevMode 강제 false _vm.DevMode = false; cb.IsChecked = false; } UpdateDevModeContentVisibility(); } private void DevModeCheckBox_Unchecked(object sender, RoutedEventArgs e) { UpdateDevModeContentVisibility(); } /// 개발자 모드 활성화 상태에 따라 개발자 탭 내용 표시/숨김. private void UpdateDevModeContentVisibility() { if (DevModeContent != null) DevModeContent.Visibility = _vm.DevMode ? Visibility.Visible : Visibility.Collapsed; } // ─── AI 기능 활성화 토글 ──────────────────────────────────────────────── /// AI 기능 토글 상태를 UI와 설정에 반영합니다. private void ApplyAiEnabledState(bool enabled, bool init = false) { ApplyMainTabVisibility(NormalizeDisplayMode((System.Windows.Application.Current as App)?.SettingsService?.Settings?.Llm?.AgentUiExpressionLevel)); } public void SelectAgentSettingsTab() { OpenAgentSettingsShortcut(closeAfterOpen: false); } private void BtnAgentSettingsBack_Click(object sender, RoutedEventArgs e) { if (MainSettingsTab == null) return; var fallback = MainSettingsTab.Items .OfType() .FirstOrDefault(t => t != AgentTabItem && t.Visibility == Visibility.Visible); if (fallback != null) MainSettingsTab.SelectedItem = fallback; } private void BtnAgentShortcut_Click(object sender, RoutedEventArgs e) { OpenAgentSettingsShortcut(closeAfterOpen: true); } private void MainSettingsTab_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (MainSettingsTab?.SelectedItem is not TabItem selected) return; if (!ReferenceEquals(selected, AgentShortcutTabItem)) return; OpenAgentSettingsShortcut(closeAfterOpen: true); } private void OpenAgentSettingsShortcut(bool closeAfterOpen) { var app = System.Windows.Application.Current as App; app?.OpenAgentSettingsInChat(); if (closeAfterOpen) Close(); } private void ApplyOperationModeState(string? mode) { var normalized = OperationModePolicy.Normalize(mode); if (AgentOperationModeInternal != null) AgentOperationModeInternal.IsChecked = normalized == OperationModePolicy.InternalMode; if (AgentOperationModeExternal != null) AgentOperationModeExternal.IsChecked = normalized == OperationModePolicy.ExternalMode; } private static void SyncOperationModeCombo(ComboBox? combo, string normalized) { if (combo == null) return; foreach (var item in combo.Items.OfType()) { var key = item.Tag as string; if (string.Equals(key, normalized, StringComparison.OrdinalIgnoreCase)) { if (!ReferenceEquals(combo.SelectedItem, item)) combo.SelectedItem = item; return; } } } private bool PromptPasswordDialog(string title, string header, string message) { var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60)); var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40)); var dlg = new Window { Title = title, Width = 340, SizeToContent = SizeToContent.Height, WindowStartupLocation = WindowStartupLocation.CenterOwner, Owner = this, ResizeMode = ResizeMode.NoResize, WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent, }; var border = new Border { Background = bgBrush, CornerRadius = new CornerRadius(12), BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(20), }; var stack = new StackPanel(); stack.Children.Add(new TextBlock { Text = header, FontSize = 15, FontWeight = FontWeights.SemiBold, Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 12), }); stack.Children.Add(new TextBlock { Text = message, FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 6), }); var pwBox = new PasswordBox { FontSize = 14, Padding = new Thickness(8, 6, 8, 6), Background = itemBg, Foreground = fgBrush, BorderBrush = borderBrush, PasswordChar = '*', }; stack.Children.Add(pwBox); var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0), }; var cancelBtn = new Button { Content = "취소", Padding = new Thickness(16, 6, 16, 6), Margin = new Thickness(0, 0, 8, 0) }; cancelBtn.Click += (_, _) => { dlg.DialogResult = false; }; btnRow.Children.Add(cancelBtn); var okBtn = new Button { Content = "확인", Padding = new Thickness(16, 6, 16, 6), IsDefault = true }; okBtn.Click += (_, _) => { if (pwBox.Password == UnifiedAdminPassword) dlg.DialogResult = true; else { pwBox.Clear(); pwBox.Focus(); } }; btnRow.Children.Add(okBtn); stack.Children.Add(btnRow); border.Child = stack; dlg.Content = border; dlg.Loaded += (_, _) => pwBox.Focus(); return dlg.ShowDialog() == true; } private void AiEnabled_Changed(object sender, RoutedEventArgs e) { if (!IsLoaded) return; var sourceToggle = sender as CheckBox; var tryEnable = sourceToggle?.IsChecked == true; // 비활성화는 즉시 적용 (비밀번호 불필요) if (!tryEnable) { var app2 = System.Windows.Application.Current as App; if (app2?.SettingsService?.Settings != null) { app2.SettingsService.Settings.AiEnabled = false; app2.SettingsService.Save(); } ApplyAiEnabledState(false); return; } // 이미 활성화된 상태에서 설정 창이 열릴 때 토글 복원으로 인한 이벤트 → 비밀번호 불필요 var currentApp = System.Windows.Application.Current as App; if (currentApp?.SettingsService?.Settings.AiEnabled == true) return; // 새로 활성화하는 경우에만 비밀번호 확인 var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60)); var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40)); var dlg = new Window { Title = "AI 기능 활성화 ? 비밀번호 확인", Width = 340, SizeToContent = SizeToContent.Height, WindowStartupLocation = WindowStartupLocation.CenterOwner, Owner = this, ResizeMode = ResizeMode.NoResize, WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent, }; var border = new Border { Background = bgBrush, CornerRadius = new CornerRadius(12), BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(20), }; var stack = new StackPanel(); stack.Children.Add(new TextBlock { Text = "\U0001f512 AI 기능 활성화", FontSize = 15, FontWeight = FontWeights.SemiBold, Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 12), }); stack.Children.Add(new TextBlock { Text = "비밀번호를 입력하세요:", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 6), }); var pwBox = new PasswordBox { FontSize = 14, Padding = new Thickness(8, 6, 8, 6), Background = itemBg, Foreground = fgBrush, BorderBrush = borderBrush, PasswordChar = '*', }; stack.Children.Add(pwBox); var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) }; var cancelBtn = new Button { Content = "취소", Padding = new Thickness(16, 6, 16, 6), Margin = new Thickness(0, 0, 8, 0) }; cancelBtn.Click += (_, _) => { dlg.DialogResult = false; }; btnRow.Children.Add(cancelBtn); var okBtn = new Button { Content = "확인", Padding = new Thickness(16, 6, 16, 6), IsDefault = true }; okBtn.Click += (_, _) => { if (pwBox.Password == UnifiedAdminPassword) dlg.DialogResult = true; else { pwBox.Clear(); pwBox.Focus(); } }; btnRow.Children.Add(okBtn); stack.Children.Add(btnRow); border.Child = stack; dlg.Content = border; dlg.Loaded += (_, _) => pwBox.Focus(); if (dlg.ShowDialog() == true) { var app = System.Windows.Application.Current as App; if (app?.SettingsService?.Settings != null) { app.SettingsService.Settings.AiEnabled = true; app.SettingsService.Save(); } ApplyAiEnabledState(true); } else { // 취소/실패 ? 토글 원상복구 if (AgentAiEnabledToggle != null) AgentAiEnabledToggle.IsChecked = false; } } private void OperationModeCombo_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (!IsLoaded) return; if (sender is not ComboBox combo || combo.SelectedItem is not ComboBoxItem selected) return; var mode = selected.Tag as string; if (string.IsNullOrWhiteSpace(mode)) return; var app = System.Windows.Application.Current as App; var settings = app?.SettingsService?.Settings; if (settings == null) return; var normalized = OperationModePolicy.Normalize(mode); var current = OperationModePolicy.Normalize(settings.OperationMode); if (string.Equals(normalized, current, StringComparison.OrdinalIgnoreCase)) return; var ok = PromptPasswordDialog( "운영 모드 변경 ? 비밀번호 확인", "\U0001f512 사내/사외 모드 변경", "비밀번호를 입력하세요:"); if (!ok) { ApplyOperationModeState(settings.OperationMode); return; } settings.OperationMode = normalized; app?.SettingsService?.Save(); } private void StepApprovalCheckBox_Checked(object sender, RoutedEventArgs e) { if (sender is not CheckBox cb || !cb.IsChecked.GetValueOrDefault()) return; // 설정 창 로드 중 바인딩에 의한 자동 Checked 이벤트 무시 (이미 활성화된 상태 복원) if (!IsLoaded) return; // 테마 리소스 조회 var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60)); var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40)); var dlg = new Window { Title = "스텝 바이 스텝 승인 ? 비밀번호 확인", Width = 340, SizeToContent = SizeToContent.Height, WindowStartupLocation = WindowStartupLocation.CenterOwner, Owner = this, ResizeMode = ResizeMode.NoResize, WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent, }; var border = new Border { Background = bgBrush, CornerRadius = new CornerRadius(12), BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(20), }; var stack = new StackPanel(); stack.Children.Add(new TextBlock { Text = "\U0001f50d 스텝 바이 스텝 승인 활성화", FontSize = 15, FontWeight = FontWeights.SemiBold, Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 12), }); stack.Children.Add(new TextBlock { Text = "개발자 비밀번호를 입력하세요:", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 6), }); var pwBox = new PasswordBox { FontSize = 14, Padding = new Thickness(8, 6, 8, 6), Background = itemBg, Foreground = fgBrush, BorderBrush = borderBrush, PasswordChar = '*', }; stack.Children.Add(pwBox); var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) }; var cancelBtn = new Button { Content = "취소", Padding = new Thickness(16, 6, 16, 6), Margin = new Thickness(0, 0, 8, 0) }; cancelBtn.Click += (_, _) => { dlg.DialogResult = false; }; btnRow.Children.Add(cancelBtn); var okBtn = new Button { Content = "확인", Padding = new Thickness(16, 6, 16, 6), IsDefault = true }; okBtn.Click += (_, _) => { if (pwBox.Password == UnifiedAdminPassword) dlg.DialogResult = true; else { pwBox.Clear(); pwBox.Focus(); } }; btnRow.Children.Add(okBtn); stack.Children.Add(btnRow); border.Child = stack; dlg.Content = border; dlg.Loaded += (_, _) => pwBox.Focus(); if (dlg.ShowDialog() != true) { cb.IsChecked = false; } } private void BtnClearMemory_Click(object sender, RoutedEventArgs e) { var result = CustomMessageBox.Show( "에이전트 메모리를 초기화하면 학습된 모든 규칙과 선호도가 삭제됩니다.\n계속하시겠습니까?", "에이전트 메모리 초기화", MessageBoxButton.YesNo, MessageBoxImage.Warning); if (result != MessageBoxResult.Yes) return; var app = System.Windows.Application.Current as App; app?.MemoryService?.Clear(); CustomMessageBox.Show("에이전트 메모리가 초기화되었습니다.", "완료", MessageBoxButton.OK, MessageBoxImage.Information); } // ─── 에이전트 훅 관리 ───────────────────────────────────────────────── private void AddHookBtn_Click(object sender, MouseButtonEventArgs e) { ShowHookEditDialog(null, -1); } /// 플레이스홀더(워터마크) TextBlock 생성 헬퍼. private static TextBlock CreatePlaceholder(string text, Brush foreground, string? currentValue) { return new TextBlock { Text = text, FontSize = 13, Foreground = foreground, Opacity = 0.45, IsHitTestVisible = false, VerticalAlignment = VerticalAlignment.Center, Padding = new Thickness(14, 8, 14, 8), Visibility = string.IsNullOrEmpty(currentValue) ? Visibility.Visible : Visibility.Collapsed, }; } private void ShowHookEditDialog(Models.AgentHookEntry? existing, int index) { var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60)); var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40)); var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var isNew = existing == null; var dlg = new Window { Title = isNew ? "훅 추가" : "훅 편집", Width = 420, SizeToContent = SizeToContent.Height, WindowStartupLocation = WindowStartupLocation.CenterOwner, Owner = this, ResizeMode = ResizeMode.NoResize, WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent, }; var border = new Border { Background = bgBrush, CornerRadius = new CornerRadius(12), BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(20), }; var stack = new StackPanel(); // 제목 stack.Children.Add(new TextBlock { Text = isNew ? "\u2699 훅 추가" : "\u2699 훅 편집", FontSize = 15, FontWeight = FontWeights.SemiBold, Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 14), }); // ESC 키로 닫기 dlg.KeyDown += (_, e) => { if (e.Key == System.Windows.Input.Key.Escape) dlg.Close(); }; // 훅 이름 stack.Children.Add(new TextBlock { Text = "훅 이름", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 4) }); var nameBox = new TextBox { Text = existing?.Name ?? "", FontSize = 13, Foreground = fgBrush, Background = itemBg, BorderBrush = borderBrush, Padding = new Thickness(12, 8, 12, 8), }; var nameHolder = CreatePlaceholder("예: 코드 리뷰 후 알림", subFgBrush, existing?.Name); nameBox.TextChanged += (_, _) => nameHolder.Visibility = string.IsNullOrEmpty(nameBox.Text) ? Visibility.Visible : Visibility.Collapsed; var nameGrid = new Grid(); nameGrid.Children.Add(nameBox); nameGrid.Children.Add(nameHolder); stack.Children.Add(nameGrid); // 대상 도구 stack.Children.Add(new TextBlock { Text = "대상 도구 (* = 모든 도구)", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 10, 0, 4) }); var toolBox = new TextBox { Text = existing?.ToolName ?? "*", FontSize = 13, Foreground = fgBrush, Background = itemBg, BorderBrush = borderBrush, Padding = new Thickness(12, 8, 12, 8), }; var toolHolder = CreatePlaceholder("예: file_write, grep_tool", subFgBrush, existing?.ToolName ?? "*"); toolBox.TextChanged += (_, _) => toolHolder.Visibility = string.IsNullOrEmpty(toolBox.Text) ? Visibility.Visible : Visibility.Collapsed; var toolGrid = new Grid(); toolGrid.Children.Add(toolBox); toolGrid.Children.Add(toolHolder); stack.Children.Add(toolGrid); // 타이밍 stack.Children.Add(new TextBlock { Text = "실행 타이밍", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 10, 0, 4) }); var timingPanel = new StackPanel { Orientation = Orientation.Horizontal }; var preRadio = new RadioButton { Content = "Pre (실행 전)", Foreground = fgBrush, FontSize = 13, Margin = new Thickness(0, 0, 16, 0), IsChecked = (existing?.Timing ?? "post") == "pre" }; var postRadio = new RadioButton { Content = "Post (실행 후)", Foreground = fgBrush, FontSize = 13, IsChecked = (existing?.Timing ?? "post") != "pre" }; timingPanel.Children.Add(preRadio); timingPanel.Children.Add(postRadio); stack.Children.Add(timingPanel); // 스크립트 경로 stack.Children.Add(new TextBlock { Text = "스크립트 경로 (.bat / .cmd / .ps1)", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 10, 0, 4) }); var pathGrid = new Grid(); pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var pathInnerGrid = new Grid(); var pathBox = new TextBox { Text = existing?.ScriptPath ?? "", FontSize = 13, Foreground = fgBrush, Background = itemBg, BorderBrush = borderBrush, Padding = new Thickness(12, 8, 12, 8), }; var pathHolder = CreatePlaceholder("예: C:\\scripts\\review-notify.bat", subFgBrush, existing?.ScriptPath); pathBox.TextChanged += (_, _) => pathHolder.Visibility = string.IsNullOrEmpty(pathBox.Text) ? Visibility.Visible : Visibility.Collapsed; pathInnerGrid.Children.Add(pathBox); pathInnerGrid.Children.Add(pathHolder); Grid.SetColumn(pathInnerGrid, 0); pathGrid.Children.Add(pathInnerGrid); var browseBtn = new Border { Background = itemBg, CornerRadius = new CornerRadius(6), Padding = new Thickness(10, 6, 10, 6), Margin = new Thickness(6, 0, 0, 0), Cursor = Cursors.Hand, VerticalAlignment = VerticalAlignment.Center, }; browseBtn.Child = new TextBlock { Text = "...", FontSize = 13, Foreground = accentBrush }; browseBtn.MouseLeftButtonUp += (_, _) => { var ofd = new Microsoft.Win32.OpenFileDialog { Filter = "스크립트 파일|*.bat;*.cmd;*.ps1|모든 파일|*.*", Title = "훅 스크립트 선택", }; if (ofd.ShowDialog() == true) pathBox.Text = ofd.FileName; }; Grid.SetColumn(browseBtn, 1); pathGrid.Children.Add(browseBtn); stack.Children.Add(pathGrid); // 추가 인수 stack.Children.Add(new TextBlock { Text = "추가 인수 (선택)", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 10, 0, 4) }); var argsBox = new TextBox { Text = existing?.Arguments ?? "", FontSize = 13, Foreground = fgBrush, Background = itemBg, BorderBrush = borderBrush, Padding = new Thickness(12, 8, 12, 8), }; var argsHolder = CreatePlaceholder("예: --verbose --output log.txt", subFgBrush, existing?.Arguments); argsBox.TextChanged += (_, _) => argsHolder.Visibility = string.IsNullOrEmpty(argsBox.Text) ? Visibility.Visible : Visibility.Collapsed; var argsGrid = new Grid(); argsGrid.Children.Add(argsBox); argsGrid.Children.Add(argsHolder); stack.Children.Add(argsGrid); // 버튼 행 var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) }; var cancelBorder = new Border { Background = itemBg, CornerRadius = new CornerRadius(8), Padding = new Thickness(16, 8, 16, 8), Margin = new Thickness(0, 0, 8, 0), Cursor = Cursors.Hand, }; cancelBorder.Child = new TextBlock { Text = "취소", FontSize = 13, Foreground = subFgBrush }; cancelBorder.MouseLeftButtonUp += (_, _) => dlg.Close(); btnRow.Children.Add(cancelBorder); var saveBorder = new Border { Background = accentBrush, CornerRadius = new CornerRadius(8), Padding = new Thickness(16, 8, 16, 8), Cursor = Cursors.Hand, }; saveBorder.Child = new TextBlock { Text = isNew ? "추가" : "저장", FontSize = 13, Foreground = Brushes.White, FontWeight = FontWeights.SemiBold }; saveBorder.MouseLeftButtonUp += (_, _) => { if (string.IsNullOrWhiteSpace(nameBox.Text) || string.IsNullOrWhiteSpace(pathBox.Text)) { CustomMessageBox.Show("훅 이름과 스크립트 경로를 입력하세요.", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning); return; } var entry = new Models.AgentHookEntry { Name = nameBox.Text.Trim(), ToolName = string.IsNullOrWhiteSpace(toolBox.Text) ? "*" : toolBox.Text.Trim(), Timing = preRadio.IsChecked == true ? "pre" : "post", ScriptPath = pathBox.Text.Trim(), Arguments = argsBox.Text.Trim(), Enabled = existing?.Enabled ?? true, }; var hooks = _vm.Service.Settings.Llm.AgentHooks; if (isNew) hooks.Add(entry); else if (index >= 0 && index < hooks.Count) hooks[index] = entry; BuildHookCards(); dlg.Close(); }; btnRow.Children.Add(saveBorder); stack.Children.Add(btnRow); border.Child = stack; dlg.Content = border; dlg.ShowDialog(); } private void BuildHookCards() { if (HookListPanel == null) return; HookListPanel.Children.Clear(); var hooks = _vm.Service.Settings.Llm.AgentHooks; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue; for (int i = 0; i < hooks.Count; i++) { var hook = hooks[i]; var idx = i; var card = new Border { Background = TryFindResource("ItemBackground") as Brush ?? Brushes.LightGray, CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 8, 10, 8), Margin = new Thickness(0, 0, 0, 4), }; var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // 토글 grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); // 정보 grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // 편집 grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // 삭제 // 토글 var toggle = new CheckBox { IsChecked = hook.Enabled, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), Style = TryFindResource("ToggleSwitch") as Style, }; var capturedHook = hook; toggle.Checked += (_, _) => capturedHook.Enabled = true; toggle.Unchecked += (_, _) => capturedHook.Enabled = false; Grid.SetColumn(toggle, 0); grid.Children.Add(toggle); // 정보 var info = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; var timingBadge = hook.Timing == "pre" ? "PRE" : "POST"; var timingColor = hook.Timing == "pre" ? "#FF9800" : "#4CAF50"; var headerPanel = new StackPanel { Orientation = Orientation.Horizontal }; headerPanel.Children.Add(new TextBlock { Text = hook.Name, FontSize = 13, FontWeight = FontWeights.SemiBold, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, }); headerPanel.Children.Add(new Border { Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString(timingColor)), CornerRadius = new CornerRadius(4), Padding = new Thickness(5, 1, 5, 1), Margin = new Thickness(6, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center, Child = new TextBlock { Text = timingBadge, FontSize = 9, Foreground = Brushes.White, FontWeight = FontWeights.Bold }, }); if (hook.ToolName != "*") { headerPanel.Children.Add(new Border { Background = new SolidColorBrush(Color.FromArgb(40, 100, 100, 255)), CornerRadius = new CornerRadius(4), Padding = new Thickness(5, 1, 5, 1), Margin = new Thickness(4, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center, Child = new TextBlock { Text = hook.ToolName, FontSize = 9, Foreground = accentBrush }, }); } info.Children.Add(headerPanel); info.Children.Add(new TextBlock { Text = System.IO.Path.GetFileName(hook.ScriptPath), FontSize = 11, Foreground = secondaryText, TextTrimming = TextTrimming.CharacterEllipsis, MaxWidth = 200, }); Grid.SetColumn(info, 1); grid.Children.Add(info); // 편집 버튼 var editBtn = new Border { Cursor = Cursors.Hand, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(4, 0, 4, 0), Padding = new Thickness(6), }; editBtn.Child = new TextBlock { Text = "\uE70F", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = secondaryText, }; editBtn.MouseLeftButtonUp += (_, _) => ShowHookEditDialog(hooks[idx], idx); Grid.SetColumn(editBtn, 2); grid.Children.Add(editBtn); // 삭제 버튼 var delBtn = new Border { Cursor = Cursors.Hand, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 0, 0), Padding = new Thickness(6), }; delBtn.Child = new TextBlock { Text = "\uE74D", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = new SolidColorBrush(Color.FromRgb(0xEF, 0x53, 0x50)), }; delBtn.MouseLeftButtonUp += (_, _) => { hooks.RemoveAt(idx); BuildHookCards(); }; Grid.SetColumn(delBtn, 3); grid.Children.Add(delBtn); card.Child = grid; HookListPanel.Children.Add(card); } } // ─── MCP 서버 관리 ───────────────────────────────────────────────── private void BtnAddMcpServer_Click(object sender, RoutedEventArgs e) { var dlg = new InputDialog("MCP 서버 추가", "서버 이름:", placeholder: "예: my-mcp-server"); dlg.Owner = this; if (dlg.ShowDialog() != true || string.IsNullOrWhiteSpace(dlg.ResponseText)) return; var name = dlg.ResponseText.Trim(); var cmdDlg = new InputDialog("MCP 서버 추가", "실행 명령:", placeholder: "예: npx -y @modelcontextprotocol/server-filesystem"); cmdDlg.Owner = this; if (cmdDlg.ShowDialog() != true || string.IsNullOrWhiteSpace(cmdDlg.ResponseText)) return; var entry = new Models.McpServerEntry { Name = name, Command = cmdDlg.ResponseText.Trim(), Enabled = true }; _vm.Service.Settings.Llm.McpServers.Add(entry); BuildMcpServerCards(); } private void BuildMcpServerCards() { if (McpServerListPanel == null) return; McpServerListPanel.Children.Clear(); var servers = _vm.Service.Settings.Llm.McpServers; var primaryText = TryFindResource("PrimaryText") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Black; var secondaryText = TryFindResource("SecondaryText") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Gray; var accentBrush = TryFindResource("AccentColor") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Blue; for (int i = 0; i < servers.Count; i++) { var srv = servers[i]; var idx = i; var card = new Border { Background = TryFindResource("ItemBackground") as System.Windows.Media.Brush, CornerRadius = new CornerRadius(10), Padding = new Thickness(14, 10, 14, 10), Margin = new Thickness(0, 4, 0, 0), BorderBrush = TryFindResource("BorderColor") as System.Windows.Media.Brush, BorderThickness = new Thickness(1), }; var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var info = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; info.Children.Add(new TextBlock { Text = srv.Name, FontSize = 13.5, FontWeight = FontWeights.SemiBold, Foreground = primaryText, }); var detailSp = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 0) }; detailSp.Children.Add(new Border { Background = srv.Enabled ? accentBrush : System.Windows.Media.Brushes.Gray, CornerRadius = new CornerRadius(4), Padding = new Thickness(6, 1, 6, 1), Margin = new Thickness(0, 0, 8, 0), Opacity = 0.8, Child = new TextBlock { Text = srv.Enabled ? "활성" : "비활성", FontSize = 10, Foreground = System.Windows.Media.Brushes.White, FontWeight = FontWeights.SemiBold }, }); detailSp.Children.Add(new TextBlock { Text = $"{srv.Command} {string.Join(" ", srv.Args)}", FontSize = 11, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, MaxWidth = 300, TextTrimming = TextTrimming.CharacterEllipsis, }); info.Children.Add(detailSp); Grid.SetColumn(info, 0); grid.Children.Add(info); var btnPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center }; // 활성/비활성 토글 var toggleBtn = new Button { Content = srv.Enabled ? "\uE73E" : "\uE711", FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), FontSize = 12, ToolTip = srv.Enabled ? "비활성화" : "활성화", Background = System.Windows.Media.Brushes.Transparent, BorderThickness = new Thickness(0), Foreground = srv.Enabled ? accentBrush : System.Windows.Media.Brushes.Gray, Padding = new Thickness(6, 4, 6, 4), Cursor = Cursors.Hand, }; toggleBtn.Click += (_, _) => { servers[idx].Enabled = !servers[idx].Enabled; BuildMcpServerCards(); }; btnPanel.Children.Add(toggleBtn); // 삭제 var delBtn = new Button { Content = "\uE74D", FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), FontSize = 12, ToolTip = "삭제", Background = System.Windows.Media.Brushes.Transparent, BorderThickness = new Thickness(0), Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0xDD, 0x44, 0x44)), Padding = new Thickness(6, 4, 6, 4), Cursor = Cursors.Hand, }; delBtn.Click += (_, _) => { servers.RemoveAt(idx); BuildMcpServerCards(); }; btnPanel.Children.Add(delBtn); Grid.SetColumn(btnPanel, 1); grid.Children.Add(btnPanel); card.Child = grid; McpServerListPanel.Children.Add(card); } } // ─── 감사 로그 폴더 열기 ──────────────────────────────────────────── private void BtnOpenAuditLog_Click(object sender, RoutedEventArgs e) { try { System.Diagnostics.Process.Start("explorer.exe", Services.AuditLogService.GetAuditFolder()); } catch { } } // ─── 폴백/MCP 텍스트 박스 로드/저장 ─────────────────────────────────── private void BuildFallbackModelsPanel() { if (FallbackModelsPanel == null) return; FallbackModelsPanel.Children.Clear(); var llm = _vm.Service.Settings.Llm; var fallbacks = llm.FallbackModels; var toggleStyle = TryFindResource("ToggleSwitch") as Style; // 서비스별로 모델 수집 (순서 고정: Ollama → vLLM → Gemini → Claude) var sections = new (string Service, string Label, string Color, List Models)[] { ("ollama", "Ollama", "#107C10", new()), ("vllm", "vLLM", "#0078D4", new()), ("gemini", "Gemini", "#4285F4", new()), ("claude", "Claude", "#8B5CF6", new()), }; // RegisteredModels → ViewModel과 AppSettings 양쪽에서 수집 (저장 전에도 반영) // 1) ViewModel의 RegisteredModels (UI에서 방금 추가한 것 포함) foreach (var row in _vm.RegisteredModels) { var svc = (row.Service ?? "").ToLowerInvariant(); if (svc == string.Concat("cl", "aude")) svc = "claude"; var modelName = !string.IsNullOrEmpty(row.Alias) ? row.Alias : row.EncryptedModelName; var section = sections.FirstOrDefault(s => s.Service == svc); if (section.Models != null && !string.IsNullOrEmpty(modelName) && !section.Models.Contains(modelName)) section.Models.Add(modelName); } // 2) AppSettings의 RegisteredModels (기존 저장된 것 ? ViewModel에 없는 경우 보완) foreach (var m in llm.RegisteredModels) { var svc = (m.Service ?? "").ToLowerInvariant(); if (svc == string.Concat("cl", "aude")) svc = "claude"; var modelName = !string.IsNullOrEmpty(m.Alias) ? m.Alias : m.EncryptedModelName; var section = sections.FirstOrDefault(s => s.Service == svc); if (section.Models != null && !string.IsNullOrEmpty(modelName) && !section.Models.Contains(modelName)) section.Models.Add(modelName); } // 현재 활성 모델 추가 (중복 제거) if (!string.IsNullOrEmpty(llm.OllamaModel) && !sections[0].Models.Contains(llm.OllamaModel)) sections[0].Models.Add(llm.OllamaModel); if (!string.IsNullOrEmpty(llm.VllmModel) && !sections[1].Models.Contains(llm.VllmModel)) sections[1].Models.Add(llm.VllmModel); // Gemini/Claude 고정 모델 목록 foreach (var gm in new[] { "gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash" }) if (!sections[2].Models.Contains(gm)) sections[2].Models.Add(gm); foreach (var cm in new[] { string.Concat("cl", "aude-sonnet-4-6"), string.Concat("cl", "aude-opus-4-6"), string.Concat("cl", "aude-haiku-4-5"), string.Concat("cl", "aude-sonnet-4-5"), }) if (!sections[3].Models.Contains(cm)) sections[3].Models.Add(cm); // 렌더링 ? 모델이 없는 섹션도 헤더는 표시 foreach (var (service, svcLabel, svcColor, models) in sections) { FallbackModelsPanel.Children.Add(new TextBlock { Text = svcLabel, FontSize = 11, FontWeight = FontWeights.SemiBold, Foreground = BrushFromHex(svcColor), Margin = new Thickness(0, 8, 0, 4), }); if (models.Count == 0) { FallbackModelsPanel.Children.Add(new TextBlock { Text = "등록된 모델 없음", FontSize = 11, Foreground = Brushes.Gray, FontStyle = FontStyles.Italic, Margin = new Thickness(8, 2, 0, 4), }); continue; } foreach (var modelName in models) { var fullKey = $"{service}:{modelName}"; var row = new Grid { Margin = new Thickness(8, 2, 0, 2) }; row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var label = new TextBlock { Text = modelName, FontSize = 12, FontFamily = new FontFamily("Consolas, Courier New"), VerticalAlignment = VerticalAlignment.Center, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, }; Grid.SetColumn(label, 0); row.Children.Add(label); var captured = fullKey; var cb = new CheckBox { IsChecked = fallbacks.Contains(fullKey, StringComparer.OrdinalIgnoreCase), HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center, }; if (toggleStyle != null) cb.Style = toggleStyle; cb.Checked += (_, _) => { if (!fallbacks.Contains(captured)) fallbacks.Add(captured); FallbackModelsBox.Text = string.Join("\n", fallbacks); }; cb.Unchecked += (_, _) => { fallbacks.RemoveAll(x => x.Equals(captured, StringComparison.OrdinalIgnoreCase)); FallbackModelsBox.Text = string.Join("\n", fallbacks); }; Grid.SetColumn(cb, 1); row.Children.Add(cb); FallbackModelsPanel.Children.Add(row); } } } private void LoadAdvancedSettings() { var llm = _vm.Service.Settings.Llm; if (FallbackModelsBox != null) FallbackModelsBox.Text = string.Join("\n", llm.FallbackModels); BuildFallbackModelsPanel(); if (McpServersBox != null) { try { var json = System.Text.Json.JsonSerializer.Serialize(llm.McpServers, new System.Text.Json.JsonSerializerOptions { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); McpServersBox.Text = json; } catch { McpServersBox.Text = "[]"; } } BuildMcpServerCards(); BuildHookCards(); } private void SaveAdvancedSettings() { var llm = _vm.Service.Settings.Llm; if (FallbackModelsBox != null) { llm.FallbackModels = FallbackModelsBox.Text .Split('\n', StringSplitOptions.RemoveEmptyEntries) .Select(s => s.Trim()) .Where(s => s.Length > 0) .ToList(); } if (McpServersBox != null && !string.IsNullOrWhiteSpace(McpServersBox.Text)) { try { llm.McpServers = System.Text.Json.JsonSerializer.Deserialize>( McpServersBox.Text) ?? new(); } catch { /* JSON 파싱 실패 시 기존 유지 */ } } // 도구 비활성 목록 저장 if (_toolCardsLoaded) llm.DisabledTools = _disabledTools.ToList(); } private void AddSnippet_Click(object sender, RoutedEventArgs e) { if (!_vm.AddSnippet()) CustomMessageBox.Show( "키워드와 내용은 필수 항목입니다.\n동일한 키워드가 이미 존재하면 추가할 수 없습니다.", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning); } private void DeleteSnippet_Click(object sender, RoutedEventArgs e) { if (sender is Button btn && btn.Tag is ViewModels.SnippetRowModel row) _vm.RemoveSnippet(row); } private void Browse_Click(object sender, RoutedEventArgs e) => _vm.BrowseTarget(); private void AddShortcut_Click(object sender, RoutedEventArgs e) { if (!_vm.AddShortcut()) CustomMessageBox.Show( "키워드와 실행 대상은 필수 항목입니다.\n이미 동일한 키워드가 존재하면 추가할 수 없습니다.", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning); } private void DeleteShortcut_Click(object sender, RoutedEventArgs e) { if (sender is Button btn && btn.Tag is ViewModels.AppShortcutModel shortcut) _vm.RemoveShortcut(shortcut); } private void AddBatchCommand_Click(object sender, RoutedEventArgs e) { if (!_vm.AddBatchCommand()) CustomMessageBox.Show( "키워드와 명령어는 필수 항목입니다.\n동일한 키워드가 이미 존재하면 추가할 수 없습니다.", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning); } private void DeleteBatchCommand_Click(object sender, RoutedEventArgs e) { if (sender is Button btn && btn.Tag is ViewModels.BatchCommandModel cmd) _vm.RemoveBatchCommand(cmd); } private void ResetCommandAliases_Click(object sender, RoutedEventArgs e) { _vm.ResetSystemCommandAliases(); } private void AddIndexPath_Click(object sender, RoutedEventArgs e) { var text = NewIndexPathBox?.Text?.Trim() ?? ""; if (!string.IsNullOrWhiteSpace(text)) { _vm.AddIndexPath(text); if (NewIndexPathBox != null) NewIndexPathBox.Text = ""; } } private void BrowseIndexPath_Click(object sender, RoutedEventArgs e) => _vm.BrowseIndexPath(); private void ResetCapPrefix_Click(object sender, RoutedEventArgs e) => _vm.ResetCapPrefix(); private void ResetCapGlobalHotkey_Click(object sender, RoutedEventArgs e) => _vm.ResetCapGlobalHotkey(); private void CapHotkeyRecorder_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) { e.Handled = true; var key = e.Key == System.Windows.Input.Key.System ? e.SystemKey : e.Key; // 수정자만 입력된 경우 무시 if (key is System.Windows.Input.Key.LeftCtrl or System.Windows.Input.Key.RightCtrl or System.Windows.Input.Key.LeftAlt or System.Windows.Input.Key.RightAlt or System.Windows.Input.Key.LeftShift or System.Windows.Input.Key.RightShift or System.Windows.Input.Key.LWin or System.Windows.Input.Key.RWin) return; if (key == System.Windows.Input.Key.Escape) return; var parts = new List(); if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Control)) parts.Add("Ctrl"); if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Alt)) parts.Add("Alt"); if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Shift)) parts.Add("Shift"); if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Windows)) parts.Add("Win"); var keyName = key switch { System.Windows.Input.Key.Snapshot => "PrintScreen", System.Windows.Input.Key.Scroll => "ScrollLock", System.Windows.Input.Key.Pause => "Pause", _ => key.ToString() }; parts.Add(keyName); var hotkey = string.Join("+", parts); if (AxCopilot.Core.HotkeyParser.TryParse(hotkey, out _)) _vm.CapGlobalHotkey = hotkey; } private void ReminderCorner_Click(object sender, RoutedEventArgs e) { if (sender is System.Windows.Controls.RadioButton rb && rb.Tag is string corner) _vm.ReminderCorner = corner; } private void ReminderPreview_Click(object sender, RoutedEventArgs e) { try { var svc = _vm.Service; var (text, author) = Services.QuoteService.GetRandom(svc.Settings.Reminder.EnabledCategories); var usage = TimeSpan.FromMinutes(42); // 샘플 값 // 현재 편집 중인 값을 임시로 적용 var cfg = svc.Settings.Reminder; var origCorner = cfg.Corner; var origSeconds = cfg.DisplaySeconds; cfg.Corner = _vm.ReminderCorner; cfg.DisplaySeconds = _vm.ReminderDisplaySeconds; var popup = new ReminderPopupWindow(text, author, usage, svc); popup.Show(); cfg.Corner = origCorner; cfg.DisplaySeconds = origSeconds; } catch (Exception ex) { Services.LogService.Warn($"알림 미리보기 실패: {ex.Message}"); } } private void RemoveIndexPath_Click(object sender, RoutedEventArgs e) { if (sender is Button btn && btn.Tag is string path) _vm.RemoveIndexPath(path); } private void AddExtension_Click(object sender, RoutedEventArgs e) { var text = NewExtensionBox?.Text?.Trim() ?? ""; if (!string.IsNullOrWhiteSpace(text)) { _vm.AddExtension(text); if (NewExtensionBox != null) NewExtensionBox.Text = ""; } } private void RemoveExtension_Click(object sender, RoutedEventArgs e) { if (sender is Button btn && btn.Tag is string ext) _vm.RemoveExtension(ext); } private void Save_Click(object sender, RoutedEventArgs e) { SaveAdvancedSettings(); // 폴백/MCP 텍스트 박스 → settings 저장 _vm.Save(); CustomMessageBox.Show("설정이 저장되었습니다.", "AX Copilot", MessageBoxButton.OK, MessageBoxImage.Information); } private void Cancel_Click(object sender, RoutedEventArgs e) { Close(); } private void ExportSettings_Click(object sender, RoutedEventArgs e) { var dlg = new Microsoft.Win32.SaveFileDialog { Title = "설정 내보내기", Filter = "AX Copilot 설정|*.axsettings", FileName = $"AxCopilot_Settings_{DateTime.Now:yyyyMMdd}", DefaultExt = ".axsettings" }; if (dlg.ShowDialog() != true) return; try { var srcPath = SettingsService.SettingsPath; if (System.IO.File.Exists(srcPath)) { System.IO.File.Copy(srcPath, dlg.FileName, overwrite: true); CustomMessageBox.Show( $"설정이 내보내졌습니다:\n{dlg.FileName}", "AX Copilot", MessageBoxButton.OK, MessageBoxImage.Information); } } catch (Exception ex) { CustomMessageBox.Show($"내보내기 실패: {ex.Message}", "오류"); } } private void ImportSettings_Click(object sender, RoutedEventArgs e) { var dlg = new Microsoft.Win32.OpenFileDialog { Title = "설정 불러오기", Filter = "AX Copilot 설정|*.axsettings|모든 파일|*.*" }; if (dlg.ShowDialog() != true) return; var result = CustomMessageBox.Show( "설정을 불러오면 현재 설정이 덮어씌워집니다.\n계속하시겠습니까?", "AX Copilot ? 설정 불러오기", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result != System.Windows.MessageBoxResult.Yes) return; try { var fileContent = System.IO.File.ReadAllText(dlg.FileName); // 암호화된 파일인지 판별: PortableDecrypt 시도 var json = CryptoService.PortableDecrypt(fileContent); if (string.IsNullOrEmpty(json)) { // 평문 JSON일 수 있음 ? 직접 파싱 시도 try { var test = System.Text.Json.JsonSerializer.Deserialize(fileContent); if (test != null) json = fileContent; } catch { } } if (string.IsNullOrEmpty(json)) { CustomMessageBox.Show("유효하지 않은 설정 파일입니다.", "오류"); return; } // 유효한 설정인지 최종 확인 var settings = System.Text.Json.JsonSerializer.Deserialize(json); if (settings == null) { CustomMessageBox.Show("설정 파일을 파싱할 수 없습니다.", "오류"); return; } // 암호화하여 저장 var encrypted = CryptoService.PortableEncrypt(json); System.IO.File.WriteAllText(SettingsService.SettingsPath, encrypted); CustomMessageBox.Show( "설정이 불러와졌습니다.\n변경 사항을 적용하려면 앱을 재시작하세요.", "AX Copilot", MessageBoxButton.OK, MessageBoxImage.Information); Close(); } catch (Exception ex) { CustomMessageBox.Show($"불러오기 실패: {ex.Message}", "오류"); } } protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { // 저장하지 않고 닫아도 확인 없이 바로 닫힘 (revert는 OnClosed에서 처리) base.OnClosing(e); } protected override void OnClosed(EventArgs e) { if (!_saved) _revertCallback(); base.OnClosed(e); } }