From 08524466d2c1c91dbd50a46fd3cd7fb3985b3f97 Mon Sep 17 00:00:00 2001 From: lacvet Date: Fri, 3 Apr 2026 18:37:54 +0900 Subject: [PATCH] =?UTF-8?q?[Phase=2037-38]=20ChatWindow=C2=B7SettingsWindo?= =?UTF-8?q?w=20=ED=8C=8C=EC=85=9C=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EB=B6=84=ED=95=A0=20+=20=EC=BD=94=EB=93=9C=20=ED=92=88?= =?UTF-8?q?=EC=A7=88=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 37 — ChatWindow God Class 파셜 분할 (10,184 → 4,767줄, -53%) - ChatWindow.MessageRendering.cs (522줄): 메시지 렌더링, 체크 아이콘 - ChatWindow.SlashCommands.cs (579줄): 슬래시 명령, 드래그앤드롭 - ChatWindow.AgentSupport.cs (475줄): 에이전트 루프, 시스템 프롬프트 - ChatWindow.TaskDecomposition.cs (1,170줄): Plan UI, Diff, 이벤트 배너 - ChatWindow.Presets.cs (1,280줄): 프리셋, 하단바, 설정 토글 - ChatWindow.ModelSelector.cs (395줄): 모델 선택, 대화 관리 - ChatWindow.PreviewAndFiles.cs (1,105줄): 미리보기, 파일 탐색기 Phase 38 — SettingsWindow 파셜 분할 (3,216 → 373줄, -88%) - SettingsWindow.UI.cs (802줄): 탭 전환, 독바, 스토리지, 핫키 - SettingsWindow.Tools.cs (875줄): 도구 카드 UI, AX Agent 탭 - SettingsWindow.AgentConfig.cs (1,202줄): 모델, 스킬, 훅, MCP Phase 35-36 — 코드 품질 심층 정리 - bare catch 전량 → catch (Exception) (109개 파일) - ColorConverter → ThemeResourceHelper.HexBrush() (81건) - Application.Current as App → CurrentApp 프로퍼티 (15개 파일) - AgentContext.Settings DI 주입 (11개 에이전트 도구) - PopupMenuHelper 실제 적용 (4개 팝업) CLAUDE.md: 작업 후 깃 푸시 + 오류 시 롤백 지침 추가 docs: TECHNOLOGY_OVERVIEW.md 신규 작성 (762줄 기술 문서) 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 40 + docs/NEXT_ROADMAP.md | 18 +- .../Views/SettingsWindow.AgentConfig.cs | 1202 +++++++ src/AxCopilot/Views/SettingsWindow.Tools.cs | 875 +++++ src/AxCopilot/Views/SettingsWindow.UI.cs | 802 +++++ src/AxCopilot/Views/SettingsWindow.xaml.cs | 2843 ----------------- 6 files changed, 2936 insertions(+), 2844 deletions(-) create mode 100644 src/AxCopilot/Views/SettingsWindow.AgentConfig.cs create mode 100644 src/AxCopilot/Views/SettingsWindow.Tools.cs create mode 100644 src/AxCopilot/Views/SettingsWindow.UI.cs diff --git a/CLAUDE.md b/CLAUDE.md index 336b2d9..9ae8714 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,6 +4,46 @@ --- +## 0. 작업 완료 후 깃 푸시 규칙 + +### 매 작업 단위 완료 시 반드시 깃 푸시 +- Phase 작업(기능 개발, 리팩터링 등) 완료 → `dotnet build` 확인 → 소스 파일만 스테이징 → 커밋 → 푸시 +- **빌드 오류 없이 커밋** — `경고 0, 오류 0` 상태에서만 푸시 +- 커밋 메시지: `[PhaseXX] 작업 내용 요약 (1~2줄)` + +### 오류 복구 불가 시 이전 버전 롤백 +작업 중 오류가 복구되지 않으면 깃에서 이전 버전을 받아 작업: +```bash +# 마지막 커밋으로 전체 복구 +git reset --hard HEAD + +# 특정 커밋으로 복구 (git log로 커밋 해시 확인) +git reset --hard <커밋해시> + +# 원격 최신 버전으로 완전 복구 +git fetch origin +git reset --hard origin/main +``` +- 복구 시도 2회 이상 실패 → 즉시 롤백, 사용자에게 알림 +- 롤백 후 원인 분석 → 더 작은 단위로 재작업 + +### 스테이징 규칙 (빌드 산출물 제외) +```bash +# 소스 코드만 스테이징 (bin/, obj/ 제외) +git add src/AxCopilot/Views/ +git add src/AxCopilot/Services/ +git add src/AxCopilot/Models/ +git add src/AxCopilot/Handlers/ +git add src/AxCopilot/ViewModels/ +git add src/AxCopilot/Themes/ +git add src/AxCopilot/Core/ +git add docs/ +git add CLAUDE.md +# 절대 추가 금지: bin/, obj/, *.dll, *.exe, *.pdb +``` + +--- + ## 1. UI/UX 디자인 원칙 ### 기본 컨트롤 사용 금지 diff --git a/docs/NEXT_ROADMAP.md b/docs/NEXT_ROADMAP.md index eba5ef3..6670213 100644 --- a/docs/NEXT_ROADMAP.md +++ b/docs/NEXT_ROADMAP.md @@ -4538,5 +4538,21 @@ Week 8: [23] AutoCompact + isEnabled + 최종 검증 --- -최종 업데이트: 2026-04-03 (Phase 22~37 구현 완료 — CC 동등성 37/37 + 코드 품질 리팩터링 5차) +## Phase 38 — SettingsWindow 파셜 클래스 분할 (v2.3) ✅ 완료 + +> **목표**: 3,216줄 SettingsWindow.xaml.cs를 3개 파셜 클래스 파일로 분할. + +| 파일 | 줄 수 | 내용 | +|------|-------|------| +| `SettingsWindow.xaml.cs` (메인) | 373 | 생성자, 필드, 저장/닫기, 스니펫 이벤트 | +| `SettingsWindow.UI.cs` | 802 | 섹션 헬퍼, 탭 전환, 독바, 스토리지, 핫키, 버전 | +| `SettingsWindow.Tools.cs` | 875 | 도구/커넥터 카드 UI, AX Agent 탭, 도구 관리 | +| `SettingsWindow.AgentConfig.cs` | 1,202 | 모델 등록, 스킬, 템플릿, AI토글, 네트워크모드, 훅, MCP | + +- **메인 파일**: 3,216줄 → 373줄 (**88.4% 감소**) +- **빌드**: 경고 0, 오류 0 + +--- + +최종 업데이트: 2026-04-03 (Phase 22~38 구현 완료 — CC 동등성 37/37 + 코드 품질 리팩터링 6차) diff --git a/src/AxCopilot/Views/SettingsWindow.AgentConfig.cs b/src/AxCopilot/Views/SettingsWindow.AgentConfig.cs new file mode 100644 index 0000000..7f8f113 --- /dev/null +++ b/src/AxCopilot/Views/SettingsWindow.AgentConfig.cs @@ -0,0 +1,1202 @@ +using System.Linq; +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 +{ + // ─── 등록 모델 관리 ────────────────────────────────────────────────── + + private bool IsEncryptionEnabled => + CurrentApp?.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 (SvcTabClaude?.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 (Exception) { } + } + + // ─── 스킬 가져오기/내보내기 ────────────────────────────────────────── + + 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 = ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexBrush("#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 = Services.CryptoService.EncryptIfEnabled(dlg.ApiKey, IsEncryptionEnabled), + AuthType = dlg.AuthType, + Cp4dUrl = dlg.Cp4dUrl, + Cp4dUsername = dlg.Cp4dUsername, + Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsEncryptionEnabled), + }); + BuildFallbackModelsPanel(); + } + } + + 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 currentApiKey = Services.CryptoService.DecryptIfEnabled(row.ApiKey ?? "", IsEncryptionEnabled); + string cp4dPw = Services.CryptoService.DecryptIfEnabled(row.Cp4dPassword ?? "", IsEncryptionEnabled); + + var currentService = GetCurrentServiceSubTab(); + var dlg = new ModelRegistrationDialog(currentService, row.Alias, currentModel, + row.Endpoint, currentApiKey, + 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 = Services.CryptoService.EncryptIfEnabled(dlg.ApiKey, IsEncryptionEnabled); + row.AuthType = dlg.AuthType; + row.Cp4dUrl = dlg.Cp4dUrl; + row.Cp4dUsername = dlg.Cp4dUsername; + row.Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsEncryptionEnabled); + BuildFallbackModelsPanel(); + } + } + + 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(); + } + } + + // ─── 프롬프트 템플릿 관리 ──────────────────────────────────────────── + + 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); + } + + // ─── AI 기능 활성화 토글 ──────────────────────────────────────────────── + + /// AI 기능 토글 상태를 UI와 설정에 반영합니다. + private void ApplyAiEnabledState(bool enabled, bool init = false) + { + // 토글 스위치 체크 상태 동기화 (init 시에는 이벤트 억제) + if (AiEnabledToggle != null && AiEnabledToggle.IsChecked != enabled) + { + AiEnabledToggle.IsChecked = enabled; + } + // AX Agent 탭 가시성 + if (AgentTabItem != null) + AgentTabItem.Visibility = enabled ? Visibility.Visible : Visibility.Collapsed; + } + + private void AiEnabled_Changed(object sender, RoutedEventArgs e) + { + if (!IsLoaded) return; + var tryEnable = AiEnabledToggle?.IsChecked == true; + + // 비활성화는 즉시 적용 (비밀번호 불필요) + if (!tryEnable) + { + var app2 = CurrentApp; + if (app2?.SettingsService?.Settings != null) + { + app2.SettingsService.Settings.AiEnabled = false; + app2.SettingsService.Save(); + } + ApplyAiEnabledState(false); + return; + } + + // 이미 활성화된 상태에서 설정 창이 열릴 때 토글 복원으로 인한 이벤트 → 비밀번호 불필요 + var currentApp = CurrentApp; + 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 == SettingsPassword) + 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 = CurrentApp; + if (app?.SettingsService?.Settings != null) + { + app.SettingsService.Settings.AiEnabled = true; + app.SettingsService.Save(); + } + ApplyAiEnabledState(true); + } + else + { + // 취소/실패 — 토글 원상복구 + if (AiEnabledToggle != null) AiEnabledToggle.IsChecked = false; + } + } + + // ─── 사내/사외 모드 토글 ───────────────────────────────────────────────────── + + private const string SettingsPassword = "axgo123!"; + + private void NetworkMode_Changed(object sender, RoutedEventArgs e) + { + if (!IsLoaded) return; + var tryInternalMode = InternalModeToggle?.IsChecked == true; // true = 사내(차단), false = 사외(허용) + + // 사내 모드로 전환(차단 강화)은 비밀번호 불필요 + if (tryInternalMode) + { + var app2 = CurrentApp; + if (app2?.SettingsService?.Settings != null) + { + app2.SettingsService.Settings.InternalModeEnabled = true; + app2.SettingsService.Save(); + } + return; + } + + // 이미 사외 모드인 경우 토글 복원으로 인한 이벤트 → 비밀번호 불필요 + var currentApp = CurrentApp; + if (currentApp?.SettingsService?.Settings.InternalModeEnabled == false) 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 = "🌐 사외 모드 활성화", + FontSize = 15, FontWeight = FontWeights.SemiBold, + Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 8), + }); + stack.Children.Add(new TextBlock + { + Text = "사외 모드에서는 인터넷 검색과 외부 HTTP 접속이 허용됩니다.\n비밀번호를 입력하세요:", + FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 10), + TextWrapping = TextWrapping.Wrap, + }); + 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 == SettingsPassword) + 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 = CurrentApp; + if (app?.SettingsService?.Settings != null) + { + app.SettingsService.Settings.InternalModeEnabled = false; + app.SettingsService.Save(); + } + } + else + { + // 취소/실패 — 토글 원상복구 (사내 모드 유지) + if (InternalModeToggle != null) InternalModeToggle.IsChecked = true; + } + } + + 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 == "mouse12#") + 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 = CurrentApp; + 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 = ThemeResourceHelper.HexBrush(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 @anthropic/mcp-server"); + 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 (Exception) { } + } + + // ─── 폴백/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(); + 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(); + 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[] { "claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5", "claude-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 (Exception) { 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 (Exception) { /* JSON 파싱 실패 시 기존 유지 */ } + } + + // 도구 비활성 목록 저장 + if (_toolCardsLoaded) + llm.DisabledTools = _disabledTools.ToList(); + } +} diff --git a/src/AxCopilot/Views/SettingsWindow.Tools.cs b/src/AxCopilot/Views/SettingsWindow.Tools.cs new file mode 100644 index 0000000..288f105 --- /dev/null +++ b/src/AxCopilot/Views/SettingsWindow.Tools.cs @@ -0,0 +1,875 @@ +using System.Linq; +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 +{ + // ─── 도구/커넥터 관리 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", "프로젝트 지침 — AX.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 + ?? ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexBrush(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 + ?? ThemeResourceHelper.HexBrush("#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 + ?? ThemeResourceHelper.HexBrush("#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 + ?? ThemeResourceHelper.HexBrush("#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 + ?? ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexColor("#4B5EFC"); + var subtleText = ThemeResourceHelper.HexColor("#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 = ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexBrush("#555"), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 6, 0), + }); + exportContent.Children.Add(new TextBlock + { + Text = "스킬 내보내기", + FontSize = 12, + FontWeight = FontWeights.SemiBold, + Foreground = ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexBrush("#4B5EFC"), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 6, 0), + }); + galleryContent.Children.Add(new TextBlock + { + Text = "스킬 갤러리 열기", + FontSize = 12, + FontWeight = FontWeights.SemiBold, + Foreground = ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexBrush("#A78BFA"), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 6, 0), + }); + statsContent.Children.Add(new TextBlock + { + Text = "실행 통계 보기", + FontSize = 12, + FontWeight = FontWeights.SemiBold, + Foreground = ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexColor(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 + ?? ThemeResourceHelper.HexBrush("#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 + ?? ThemeResourceHelper.HexBrush("#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 ?? ThemeResourceHelper.HexBrush("#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 + ?? ThemeResourceHelper.HexBrush("#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; + } + + // ─── AX Agent 서브탭 전환 ─────────────────────────────────────────── + + private void AgentSubTab_Checked(object sender, RoutedEventArgs e) + { + if (AgentPanelCommon == null) return; // 초기화 전 방어 + AgentPanelCommon.Visibility = AgentTabCommon.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; + AgentPanelChat.Visibility = AgentTabChat.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; + AgentPanelCoworkCode.Visibility = AgentTabCoworkCode.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; + AgentPanelCowork.Visibility = AgentTabCowork.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; + AgentPanelCode.Visibility = AgentTabCode.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; + if (AgentPanelDev != null) + AgentPanelDev.Visibility = AgentTabDev.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; + if (AgentPanelEtc != null) + AgentPanelEtc.Visibility = AgentTabEtc.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; + if (AgentPanelTools != null) + { + var show = AgentTabTools.IsChecked == true; + AgentPanelTools.Visibility = show ? Visibility.Visible : Visibility.Collapsed; + if (show) LoadToolCards(); + } + } + + // ─── 도구 관리 카드 UI ────────────────────────────────────────────── + + private bool _toolCardsLoaded; + private HashSet _disabledTools = new(StringComparer.OrdinalIgnoreCase); + + /// 도구 카드 UI를 카테고리별로 생성합니다. + private void LoadToolCards() + { + if (_toolCardsLoaded || ToolCardsPanel == null) return; + _toolCardsLoaded = true; + + var app = CurrentApp; + 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 = CurrentApp; + 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); + } + } +} diff --git a/src/AxCopilot/Views/SettingsWindow.UI.cs b/src/AxCopilot/Views/SettingsWindow.UI.cs new file mode 100644 index 0000000..c9dd31d --- /dev/null +++ b/src/AxCopilot/Views/SettingsWindow.UI.cs @@ -0,0 +1,802 @@ +using System.Linq; +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 +{ + // ─── 접기/열기 가능 섹션 헬퍼 ────────────────────────────────────────── + private Border CreateCollapsibleSection(string title, UIElement content, bool expanded = true) + { + var headerColor = ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexBrush("#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); + } + + // ─── 선택 텍스트 AI 명령 설정 ──────────────────────────────────────────────── + + private void BuildTextActionCommandsPanel() + { + if (TextActionCommandsPanel == null) return; + TextActionCommandsPanel.Children.Clear(); + + var svc = CurrentApp?.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 = CurrentApp?.SettingsService; + if (svc == null) return; + var launcher = svc.Settings.Launcher; + + // 토글 바인딩 + ChkDockAutoShow.IsChecked = launcher.DockBarAutoShow; + ChkDockAutoShow.Checked += (_, _) => + { + launcher.DockBarAutoShow = true; svc.Save(); + CurrentApp?.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 ThemeResourceHelper.HexBrush(hex); } + catch (Exception) { return new SolidColorBrush(Colors.Gray); } + } + + private static void RefreshDock() + { + CurrentApp?.RefreshDockBar(); + } + + private void BtnDockResetPosition_Click(object sender, RoutedEventArgs e) + { + var result = CustomMessageBox.Show( + "독 바를 화면 하단 중앙으로 이동하시겠습니까?", + "독 바 위치 초기화", + MessageBoxButton.YesNo, MessageBoxImage.Question); + if (result != MessageBoxResult.Yes) return; + + var svc = CurrentApp?.SettingsService; + if (svc == null) return; + svc.Settings.Launcher.DockBarLeft = -1; + svc.Settings.Launcher.DockBarTop = -1; + svc.Save(); + + // 즉시 독 바 위치 이동 + CurrentApp?.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 (Exception) + { + 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 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; + SvcPanelClaude.Visibility = SvcTabClaude.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; + } + + 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 == "mouse12#") + 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; + } +} diff --git a/src/AxCopilot/Views/SettingsWindow.xaml.cs b/src/AxCopilot/Views/SettingsWindow.xaml.cs index 00ef26b..4880836 100644 --- a/src/AxCopilot/Views/SettingsWindow.xaml.cs +++ b/src/AxCopilot/Views/SettingsWindow.xaml.cs @@ -101,2849 +101,6 @@ public partial class SettingsWindow : Window } } - // ─── 접기/열기 가능 섹션 헬퍼 ────────────────────────────────────────── - private Border CreateCollapsibleSection(string title, UIElement content, bool expanded = true) - { - var headerColor = ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexBrush("#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", "프로젝트 지침 — AX.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 - ?? ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexBrush(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 - ?? ThemeResourceHelper.HexBrush("#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 - ?? ThemeResourceHelper.HexBrush("#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 - ?? ThemeResourceHelper.HexBrush("#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 - ?? ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexColor("#4B5EFC"); - var subtleText = ThemeResourceHelper.HexColor("#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 = ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexBrush("#555"), - VerticalAlignment = VerticalAlignment.Center, - Margin = new Thickness(0, 0, 6, 0), - }); - exportContent.Children.Add(new TextBlock - { - Text = "스킬 내보내기", - FontSize = 12, - FontWeight = FontWeights.SemiBold, - Foreground = ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexBrush("#4B5EFC"), - VerticalAlignment = VerticalAlignment.Center, - Margin = new Thickness(0, 0, 6, 0), - }); - galleryContent.Children.Add(new TextBlock - { - Text = "스킬 갤러리 열기", - FontSize = 12, - FontWeight = FontWeights.SemiBold, - Foreground = ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexBrush("#A78BFA"), - VerticalAlignment = VerticalAlignment.Center, - Margin = new Thickness(0, 0, 6, 0), - }); - statsContent.Children.Add(new TextBlock - { - Text = "실행 통계 보기", - FontSize = 12, - FontWeight = FontWeights.SemiBold, - Foreground = ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexColor(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 - ?? ThemeResourceHelper.HexBrush("#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 - ?? ThemeResourceHelper.HexBrush("#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 ?? ThemeResourceHelper.HexBrush("#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 - ?? ThemeResourceHelper.HexBrush("#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 = CurrentApp?.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 = CurrentApp?.SettingsService; - if (svc == null) return; - var launcher = svc.Settings.Launcher; - - // 토글 바인딩 - ChkDockAutoShow.IsChecked = launcher.DockBarAutoShow; - ChkDockAutoShow.Checked += (_, _) => - { - launcher.DockBarAutoShow = true; svc.Save(); - CurrentApp?.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 ThemeResourceHelper.HexBrush(hex); } - catch (Exception) { return new SolidColorBrush(Colors.Gray); } - } - - private static void RefreshDock() - { - CurrentApp?.RefreshDockBar(); - } - - private void BtnDockResetPosition_Click(object sender, RoutedEventArgs e) - { - var result = CustomMessageBox.Show( - "독 바를 화면 하단 중앙으로 이동하시겠습니까?", - "독 바 위치 초기화", - MessageBoxButton.YesNo, MessageBoxImage.Question); - if (result != MessageBoxResult.Yes) return; - - var svc = CurrentApp?.SettingsService; - if (svc == null) return; - svc.Settings.Launcher.DockBarLeft = -1; - svc.Settings.Launcher.DockBarTop = -1; - svc.Save(); - - // 즉시 독 바 위치 이동 - CurrentApp?.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 (Exception) - { - 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 => - CurrentApp?.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 (SvcTabClaude?.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 (Exception) { } - } - - // ─── 스킬 가져오기/내보내기 ────────────────────────────────────────── - - 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 = ThemeResourceHelper.HexBrush("#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 = ThemeResourceHelper.HexBrush("#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 = Services.CryptoService.EncryptIfEnabled(dlg.ApiKey, IsEncryptionEnabled), - AuthType = dlg.AuthType, - Cp4dUrl = dlg.Cp4dUrl, - Cp4dUsername = dlg.Cp4dUsername, - Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsEncryptionEnabled), - }); - BuildFallbackModelsPanel(); - } - } - - 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 currentApiKey = Services.CryptoService.DecryptIfEnabled(row.ApiKey ?? "", IsEncryptionEnabled); - string cp4dPw = Services.CryptoService.DecryptIfEnabled(row.Cp4dPassword ?? "", IsEncryptionEnabled); - - var currentService = GetCurrentServiceSubTab(); - var dlg = new ModelRegistrationDialog(currentService, row.Alias, currentModel, - row.Endpoint, currentApiKey, - 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 = Services.CryptoService.EncryptIfEnabled(dlg.ApiKey, IsEncryptionEnabled); - row.AuthType = dlg.AuthType; - row.Cp4dUrl = dlg.Cp4dUrl; - row.Cp4dUsername = dlg.Cp4dUsername; - row.Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsEncryptionEnabled); - BuildFallbackModelsPanel(); - } - } - - 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(); - } - } - - // ─── 프롬프트 템플릿 관리 ──────────────────────────────────────────── - - 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; // 초기화 전 방어 - AgentPanelCommon.Visibility = AgentTabCommon.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; - AgentPanelChat.Visibility = AgentTabChat.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; - AgentPanelCoworkCode.Visibility = AgentTabCoworkCode.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; - AgentPanelCowork.Visibility = AgentTabCowork.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; - AgentPanelCode.Visibility = AgentTabCode.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; - if (AgentPanelDev != null) - AgentPanelDev.Visibility = AgentTabDev.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; - if (AgentPanelEtc != null) - AgentPanelEtc.Visibility = AgentTabEtc.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; - if (AgentPanelTools != null) - { - var show = AgentTabTools.IsChecked == true; - AgentPanelTools.Visibility = show ? Visibility.Visible : Visibility.Collapsed; - if (show) LoadToolCards(); - } - } - - // ─── 도구 관리 카드 UI ────────────────────────────────────────────── - - private bool _toolCardsLoaded; - private HashSet _disabledTools = new(StringComparer.OrdinalIgnoreCase); - - /// 도구 카드 UI를 카테고리별로 생성합니다. - private void LoadToolCards() - { - if (_toolCardsLoaded || ToolCardsPanel == null) return; - _toolCardsLoaded = true; - - var app = CurrentApp; - 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 = CurrentApp; - 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; - SvcPanelClaude.Visibility = SvcTabClaude.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; - } - - 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 == "mouse12#") - 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) - { - // 토글 스위치 체크 상태 동기화 (init 시에는 이벤트 억제) - if (AiEnabledToggle != null && AiEnabledToggle.IsChecked != enabled) - { - AiEnabledToggle.IsChecked = enabled; - } - // AX Agent 탭 가시성 - if (AgentTabItem != null) - AgentTabItem.Visibility = enabled ? Visibility.Visible : Visibility.Collapsed; - } - - private void AiEnabled_Changed(object sender, RoutedEventArgs e) - { - if (!IsLoaded) return; - var tryEnable = AiEnabledToggle?.IsChecked == true; - - // 비활성화는 즉시 적용 (비밀번호 불필요) - if (!tryEnable) - { - var app2 = CurrentApp; - if (app2?.SettingsService?.Settings != null) - { - app2.SettingsService.Settings.AiEnabled = false; - app2.SettingsService.Save(); - } - ApplyAiEnabledState(false); - return; - } - - // 이미 활성화된 상태에서 설정 창이 열릴 때 토글 복원으로 인한 이벤트 → 비밀번호 불필요 - var currentApp = CurrentApp; - 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 == SettingsPassword) - 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 = CurrentApp; - if (app?.SettingsService?.Settings != null) - { - app.SettingsService.Settings.AiEnabled = true; - app.SettingsService.Save(); - } - ApplyAiEnabledState(true); - } - else - { - // 취소/실패 — 토글 원상복구 - if (AiEnabledToggle != null) AiEnabledToggle.IsChecked = false; - } - } - - // ─── 사내/사외 모드 토글 ───────────────────────────────────────────────────── - - private const string SettingsPassword = "axgo123!"; - - private void NetworkMode_Changed(object sender, RoutedEventArgs e) - { - if (!IsLoaded) return; - var tryInternalMode = InternalModeToggle?.IsChecked == true; // true = 사내(차단), false = 사외(허용) - - // 사내 모드로 전환(차단 강화)은 비밀번호 불필요 - if (tryInternalMode) - { - var app2 = CurrentApp; - if (app2?.SettingsService?.Settings != null) - { - app2.SettingsService.Settings.InternalModeEnabled = true; - app2.SettingsService.Save(); - } - return; - } - - // 이미 사외 모드인 경우 토글 복원으로 인한 이벤트 → 비밀번호 불필요 - var currentApp = CurrentApp; - if (currentApp?.SettingsService?.Settings.InternalModeEnabled == false) 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 = "🌐 사외 모드 활성화", - FontSize = 15, FontWeight = FontWeights.SemiBold, - Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 8), - }); - stack.Children.Add(new TextBlock - { - Text = "사외 모드에서는 인터넷 검색과 외부 HTTP 접속이 허용됩니다.\n비밀번호를 입력하세요:", - FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 10), - TextWrapping = TextWrapping.Wrap, - }); - 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 == SettingsPassword) - 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 = CurrentApp; - if (app?.SettingsService?.Settings != null) - { - app.SettingsService.Settings.InternalModeEnabled = false; - app.SettingsService.Save(); - } - } - else - { - // 취소/실패 — 토글 원상복구 (사내 모드 유지) - if (InternalModeToggle != null) InternalModeToggle.IsChecked = true; - } - } - - 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 == "mouse12#") - 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 = CurrentApp; - 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 = ThemeResourceHelper.HexBrush(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 @anthropic/mcp-server"); - 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 (Exception) { } - } - - // ─── 폴백/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(); - 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(); - 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[] { "claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5", "claude-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 (Exception) { 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 (Exception) { /* JSON 파싱 실패 시 기존 유지 */ } - } - - // 도구 비활성 목록 저장 - if (_toolCardsLoaded) - llm.DisabledTools = _disabledTools.ToList(); - } - private void AddSnippet_Click(object sender, RoutedEventArgs e) { if (!_vm.AddSnippet())