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())