using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Linq; using AxCopilot.Models; using AxCopilot.Services; using AxCopilot.Services.Agent; namespace AxCopilot.Views; public partial class AgentSettingsWindow : Window { private const string UnifiedAdminPassword = "axgo123!"; private readonly SettingsService _settings; private readonly LlmSettings _llm; private string _permissionMode = PermissionModeCatalog.Deny; private string _reasoningMode = "detailed"; private string _operationMode = OperationModePolicy.InternalMode; private string _displayMode = "rich"; private string _defaultOutputFormat = "auto"; private string _defaultMood = "modern"; private string _activePanel = "basic"; private string _selectedService = "ollama"; private string _selectedTheme = "system"; private string _selectedModel = string.Empty; private bool _toolCardsLoaded; private HashSet _disabledTools = new(StringComparer.OrdinalIgnoreCase); public AgentSettingsWindow(SettingsService settings) { _settings = settings; _llm = _settings.Settings.Llm; InitializeComponent(); LoadFromSettings(); } private void LoadFromSettings() { SkillService.EnsureSkillFolder(); SkillService.LoadSkills(_llm.SkillsFolderPath); _selectedService = (_llm.Service ?? "ollama").Trim().ToLowerInvariant(); _selectedTheme = (_llm.AgentTheme ?? "system").Trim().ToLowerInvariant(); _selectedModel = GetSelectedModelForService(_selectedService); ModelInput.Text = _selectedModel; _permissionMode = PermissionModeCatalog.NormalizeGlobalMode(_llm.FilePermission); _reasoningMode = string.IsNullOrWhiteSpace(_llm.AgentDecisionLevel) ? "detailed" : _llm.AgentDecisionLevel; _operationMode = OperationModePolicy.Normalize(_settings.Settings.OperationMode); _displayMode = "rich"; _defaultOutputFormat = string.IsNullOrWhiteSpace(_llm.DefaultOutputFormat) ? "auto" : _llm.DefaultOutputFormat; _defaultMood = string.IsNullOrWhiteSpace(_llm.DefaultMood) ? "modern" : _llm.DefaultMood; _settings.Settings.AiEnabled = true; ChkAiEnabled.IsChecked = true; ChkVllmAllowInsecureTls.IsChecked = _llm.VllmAllowInsecureTls; ChkEnableProactiveCompact.IsChecked = _llm.EnableProactiveContextCompact; TxtContextCompactTriggerPercent.Text = Math.Clamp(_llm.ContextCompactTriggerPercent, 10, 95).ToString(); TxtMaxContextTokens.Text = Math.Max(1024, _llm.MaxContextTokens).ToString(); TxtMaxRetryOnError.Text = Math.Clamp(_llm.MaxRetryOnError, 0, 10).ToString(); ChkEnableSkillSystem.IsChecked = _llm.EnableSkillSystem; ChkEnableToolHooks.IsChecked = _llm.EnableToolHooks; ChkEnableHookInputMutation.IsChecked = _llm.EnableHookInputMutation; ChkEnableHookPermissionUpdate.IsChecked = _llm.EnableHookPermissionUpdate; ChkEnableCoworkVerification.IsChecked = _llm.EnableCoworkVerification; ChkEnableProjectRules.IsChecked = _llm.EnableProjectRules; ChkEnableAgentMemory.IsChecked = _llm.EnableAgentMemory; TxtMaxAgentIterations.Text = Math.Clamp(_llm.MaxAgentIterations, 1, 200).ToString(); ChkEnableCodeVerification.IsChecked = _llm.Code.EnableCodeVerification; ChkEnableParallelTools.IsChecked = _llm.EnableParallelTools; ChkEnableWorktreeTools.IsChecked = _llm.Code.EnableWorktreeTools; ChkEnableTeamTools.IsChecked = _llm.Code.EnableTeamTools; ChkEnableCronTools.IsChecked = _llm.Code.EnableCronTools; TxtSkillsFolderPath.Text = _llm.SkillsFolderPath ?? ""; TxtSlashPopupPageSize.Text = Math.Clamp(_llm.SlashPopupPageSize, 3, 20).ToString(); ChkEnableDragDropAiActions.IsChecked = _llm.EnableDragDropAiActions; ChkDragDropAutoSend.IsChecked = _llm.DragDropAutoSend; _disabledTools = new HashSet(_llm.DisabledTools ?? new(), StringComparer.OrdinalIgnoreCase); RefreshServiceCards(); RefreshThemeCards(); RefreshModeLabels(); RefreshChatOptionLabels(); RefreshDisplayModeCards(); ShowPanel("basic"); BuildModelChips(); BuildFallbackModelsPanel(); BuildMcpServerCards(); BuildHookCards(); BuildSkillListPanel(); } private void RefreshThemeCards() { SetCardSelection(ThemeSystemCard, _selectedTheme == "system"); SetCardSelection(ThemeLightCard, _selectedTheme == "light"); SetCardSelection(ThemeDarkCard, _selectedTheme == "dark"); } private void RefreshServiceCards() { SetCardSelection(SvcOllamaCard, _selectedService == "ollama"); SetCardSelection(SvcVllmCard, _selectedService == "vllm"); SetCardSelection(SvcGeminiCard, _selectedService == "gemini"); SetCardSelection(SvcClaudeCard, _selectedService is "claude" or "sigmoid"); } private void RefreshModeLabels() { BtnOperationMode.Content = BuildOperationModeLabel(_operationMode); BtnPermissionMode.Content = PermissionModeCatalog.ToDisplayLabel(_permissionMode); BtnReasoningMode.Content = BuildReasoningModeLabel(_reasoningMode); } private void RefreshDisplayModeCards() { SetCardSelection(DisplayModeRichCard, _displayMode == "rich"); SetCardSelection(DisplayModeBalancedCard, _displayMode == "balanced"); SetCardSelection(DisplayModeSimpleCard, _displayMode == "simple"); } private void RefreshChatOptionLabels() { BtnDefaultOutputFormat.Content = BuildOutputFormatLabel(_defaultOutputFormat); BtnDefaultMood.Content = BuildMoodLabel(_defaultMood); } private void RefreshTabCards() { SetCardSelection(AgentTabBasicCard, _activePanel == "basic"); SetCardSelection(AgentTabChatCard, _activePanel == "chat"); SetCardSelection(AgentTabCoworkCard, _activePanel == "cowork"); SetCardSelection(AgentTabCodeCard, _activePanel == "code"); SetCardSelection(AgentTabDevCard, _activePanel == "dev"); SetCardSelection(AgentTabToolsCard, _activePanel == "tools"); SetCardSelection(AgentTabEtcCard, _activePanel == "etc"); } private void ShowPanel(string panel) { _activePanel = panel; PanelBasic.Visibility = panel == "basic" ? Visibility.Visible : Visibility.Collapsed; PanelChat.Visibility = panel == "chat" ? Visibility.Visible : Visibility.Collapsed; PanelCowork.Visibility = panel == "cowork" ? Visibility.Visible : Visibility.Collapsed; PanelCode.Visibility = panel == "code" ? Visibility.Visible : Visibility.Collapsed; PanelDev.Visibility = panel == "dev" ? Visibility.Visible : Visibility.Collapsed; PanelTools.Visibility = panel == "tools" ? Visibility.Visible : Visibility.Collapsed; PanelEtc.Visibility = panel == "etc" ? Visibility.Visible : Visibility.Collapsed; RefreshTabCards(); if (panel == "tools") LoadToolCards(); } private static string BuildOperationModeLabel(string mode) { return OperationModePolicy.Normalize(mode) == OperationModePolicy.ExternalMode ? "사외 모드" : "사내 모드"; } private static string BuildReasoningModeLabel(string mode) { return (mode ?? "detailed").ToLowerInvariant() switch { "minimal" => "낮음", "normal" => "중간", _ => "높음", }; } private static string BuildOutputFormatLabel(string format) { return (format ?? "auto").ToLowerInvariant() switch { "xlsx" => "Excel", "html" => "HTML 보고서", "docx" => "Word", "pptx" => "PowerPoint", "pdf" => "PDF", "md" => "Markdown", "txt" => "텍스트", _ => "AI 자동", }; } private static string BuildMoodLabel(string mood) { var found = TemplateService.AllMoods.FirstOrDefault(m => string.Equals(m.Key, mood, StringComparison.OrdinalIgnoreCase)); return found == null ? "모던" : $"{found.Icon} {found.Label}"; } private static string NormalizeDisplayMode(string? mode) { return (mode ?? "balanced").Trim().ToLowerInvariant() switch { "rich" => "rich", "simple" => "simple", _ => "balanced", }; } private void SetCardSelection(Border border, bool selected) { var accent = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue; var normal = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; border.BorderBrush = selected ? accent : normal; border.Background = selected ? (TryFindResource("HintBackground") as Brush ?? Brushes.Transparent) : Brushes.Transparent; } private void BuildModelChips() { ModelChipPanel.Children.Clear(); var models = GetModelCandidates(_selectedService); foreach (var model in models) { var captured = model; var border = new Border { Cursor = Cursors.Hand, CornerRadius = new CornerRadius(10), BorderThickness = new Thickness(1), BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray, Background = Brushes.Transparent, Padding = new Thickness(10, 6, 10, 6), Margin = new Thickness(0, 0, 8, 8), Child = new TextBlock { Text = model, FontSize = 11, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White, }, }; SetCardSelection(border, string.Equals(captured, _selectedModel, StringComparison.OrdinalIgnoreCase)); border.MouseLeftButtonUp += (_, _) => { _selectedModel = captured; ModelInput.Text = captured; BuildModelChips(); }; ModelChipPanel.Children.Add(border); } } private List GetModelCandidates(string? service) { var key = (service ?? "ollama").ToLowerInvariant(); var result = new List(); foreach (var m in _llm.RegisteredModels) { if (!string.Equals(m.Service, key, StringComparison.OrdinalIgnoreCase)) continue; if (!string.IsNullOrWhiteSpace(m.Alias) && !result.Contains(m.Alias)) result.Add(m.Alias); } void AddIf(string? value) { if (!string.IsNullOrWhiteSpace(value) && !result.Contains(value)) result.Add(value); } if (key == "ollama") AddIf(_llm.OllamaModel); else if (key == "vllm") AddIf(_llm.VllmModel); else if (key == "gemini") AddIf(_llm.GeminiModel); else AddIf(_llm.ClaudeModel); return result; } private void SetService(string service) { _selectedService = service; _selectedModel = GetSelectedModelForService(service); ModelInput.Text = _selectedModel; RefreshServiceCards(); BuildModelChips(); } private void SetTheme(string theme) { _selectedTheme = theme; RefreshThemeCards(); } private string GetSelectedModelForService(string? service) { return (service ?? "ollama").Trim().ToLowerInvariant() switch { "vllm" => _llm.VllmModel ?? "", "gemini" => _llm.GeminiModel ?? "", "claude" or "sigmoid" => _llm.ClaudeModel ?? "", _ => _llm.OllamaModel ?? "", }; } private static string CycleOperationMode(string current) { return OperationModePolicy.Normalize(current) == OperationModePolicy.ExternalMode ? OperationModePolicy.InternalMode : OperationModePolicy.ExternalMode; } private bool PromptPasswordDialog(string title, string header, string message) { 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 borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke; var dlg = new Window { Title = title, Width = 340, SizeToContent = SizeToContent.Height, WindowStartupLocation = WindowStartupLocation.CenterOwner, Owner = this, ResizeMode = ResizeMode.NoResize, WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent, ShowInTaskbar = false, }; var border = new Border { Background = bgBrush, CornerRadius = new CornerRadius(12), BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(20), }; var stack = new StackPanel(); stack.Children.Add(new TextBlock { Text = header, FontSize = 15, FontWeight = FontWeights.SemiBold, Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 12), }); stack.Children.Add(new TextBlock { Text = message, FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 6), }); var pwBox = new PasswordBox { FontSize = 14, Padding = new Thickness(8, 6, 8, 6), Background = itemBg, Foreground = fgBrush, BorderBrush = borderBrush, PasswordChar = '*', }; stack.Children.Add(pwBox); var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0), }; var cancelBtn = new Button { Content = "취소", Padding = new Thickness(16, 6, 16, 6), Margin = new Thickness(0, 0, 8, 0) }; cancelBtn.Click += (_, _) => dlg.DialogResult = false; btnRow.Children.Add(cancelBtn); var okBtn = new Button { Content = "확인", Padding = new Thickness(16, 6, 16, 6), IsDefault = true }; okBtn.Click += (_, _) => { if (pwBox.Password == UnifiedAdminPassword) dlg.DialogResult = true; else { pwBox.Clear(); pwBox.Focus(); } }; btnRow.Children.Add(okBtn); stack.Children.Add(btnRow); border.Child = stack; dlg.Content = border; dlg.Loaded += (_, _) => pwBox.Focus(); return dlg.ShowDialog() == true; } private void ThemeSystemCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetTheme("system"); private void ThemeLightCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetTheme("light"); private void ThemeDarkCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetTheme("dark"); private void DisplayModeRichCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _displayMode = "rich"; RefreshDisplayModeCards(); } private void DisplayModeBalancedCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _displayMode = "balanced"; RefreshDisplayModeCards(); } private void DisplayModeSimpleCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _displayMode = "simple"; RefreshDisplayModeCards(); } private void SvcOllamaCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetService("ollama"); private void SvcVllmCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetService("vllm"); private void SvcGeminiCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetService("gemini"); private void SvcClaudeCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetService("claude"); private void AgentTabBasicCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => ShowPanel("basic"); private void AgentTabChatCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => ShowPanel("chat"); private void AgentTabCoworkCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => ShowPanel("cowork"); private void AgentTabCodeCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => ShowPanel("code"); private void AgentTabDevCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => ShowPanel("dev"); private void AgentTabToolsCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => ShowPanel("tools"); private void AgentTabEtcCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => ShowPanel("etc"); private void BtnOperationMode_Click(object sender, RoutedEventArgs e) { var next = CycleOperationMode(_operationMode); if (!PromptPasswordDialog( "운영 모드 변경", "사내/사외 모드 변경", "비밀번호를 입력하세요.")) { return; } _operationMode = next; RefreshModeLabels(); } private void BtnPermissionMode_Click(object sender, RoutedEventArgs e) { _permissionMode = PermissionModeCatalog.NormalizeGlobalMode(_permissionMode) switch { PermissionModeCatalog.Deny => PermissionModeCatalog.Default, PermissionModeCatalog.Default => PermissionModeCatalog.AcceptEdits, PermissionModeCatalog.AcceptEdits => PermissionModeCatalog.Plan, PermissionModeCatalog.Plan => PermissionModeCatalog.BypassPermissions, // 권한 표면 모드는 코어 4단계만 순환합니다. PermissionModeCatalog.BypassPermissions => PermissionModeCatalog.Deny, PermissionModeCatalog.DontAsk => PermissionModeCatalog.Deny, _ => PermissionModeCatalog.Deny, }; RefreshModeLabels(); } private void BtnReasoningMode_Click(object sender, RoutedEventArgs e) { _reasoningMode = _reasoningMode switch { "minimal" => "normal", "normal" => "detailed", _ => "minimal", }; RefreshModeLabels(); } private void BtnDefaultOutputFormat_Click(object sender, RoutedEventArgs e) { _defaultOutputFormat = (_defaultOutputFormat ?? "auto").ToLowerInvariant() switch { "auto" => "docx", "docx" => "html", "html" => "xlsx", "xlsx" => "pdf", "pdf" => "md", "md" => "txt", _ => "auto", }; RefreshChatOptionLabels(); } private void BtnDefaultMood_Click(object sender, RoutedEventArgs e) { var moods = TemplateService.AllMoods.Select(m => m.Key).Where(k => !string.IsNullOrWhiteSpace(k)).ToList(); if (moods.Count == 0) return; var index = moods.FindIndex(k => string.Equals(k, _defaultMood, StringComparison.OrdinalIgnoreCase)); _defaultMood = index < 0 || index + 1 >= moods.Count ? moods[0] : moods[index + 1]; RefreshChatOptionLabels(); } private void BtnSave_Click(object sender, RoutedEventArgs e) { _selectedModel = string.IsNullOrWhiteSpace(ModelInput.Text) ? _selectedModel : ModelInput.Text.Trim(); _llm.Service = _selectedService; _llm.AgentTheme = _selectedTheme; _llm.FilePermission = _permissionMode; _llm.DefaultAgentPermission = _permissionMode; _llm.AgentDecisionLevel = _reasoningMode; _llm.AgentUiExpressionLevel = "rich"; _llm.DefaultOutputFormat = _defaultOutputFormat; _llm.DefaultMood = _defaultMood; _llm.VllmAllowInsecureTls = ChkVllmAllowInsecureTls.IsChecked == true; switch (_selectedService) { case "vllm": _llm.VllmModel = _selectedModel; _llm.Model = _selectedModel; break; case "gemini": _llm.GeminiModel = _selectedModel; _llm.Model = _selectedModel; break; case "claude": case "sigmoid": _llm.ClaudeModel = _selectedModel; _llm.Model = _selectedModel; break; default: _llm.OllamaModel = _selectedModel; _llm.Model = _selectedModel; break; } _llm.EnableProactiveContextCompact = ChkEnableProactiveCompact.IsChecked == true; _llm.ContextCompactTriggerPercent = ParseInt(TxtContextCompactTriggerPercent.Text, 80, 10, 95); _llm.MaxContextTokens = ParseInt(TxtMaxContextTokens.Text, 4096, 1024, 200000); _llm.MaxRetryOnError = ParseInt(TxtMaxRetryOnError.Text, 3, 0, 10); _llm.EnableSkillSystem = ChkEnableSkillSystem.IsChecked == true; _llm.EnableToolHooks = ChkEnableToolHooks.IsChecked == true; _llm.EnableHookInputMutation = ChkEnableHookInputMutation.IsChecked == true; _llm.EnableHookPermissionUpdate = ChkEnableHookPermissionUpdate.IsChecked == true; _llm.EnableCoworkVerification = ChkEnableCoworkVerification.IsChecked == true; _llm.EnableProjectRules = ChkEnableProjectRules.IsChecked == true; _llm.EnableAgentMemory = ChkEnableAgentMemory.IsChecked == true; _llm.MaxAgentIterations = ParseInt(TxtMaxAgentIterations.Text, 25, 1, 200); _llm.Code.EnableCodeVerification = ChkEnableCodeVerification.IsChecked == true; _llm.EnableParallelTools = ChkEnableParallelTools.IsChecked == true; _llm.Code.EnableWorktreeTools = ChkEnableWorktreeTools.IsChecked == true; _llm.Code.EnableTeamTools = ChkEnableTeamTools.IsChecked == true; _llm.Code.EnableCronTools = ChkEnableCronTools.IsChecked == true; _llm.SkillsFolderPath = TxtSkillsFolderPath.Text?.Trim() ?? ""; _llm.SlashPopupPageSize = ParseInt(TxtSlashPopupPageSize.Text, 7, 3, 20); _llm.EnableDragDropAiActions = ChkEnableDragDropAiActions.IsChecked == true; _llm.DragDropAutoSend = ChkDragDropAutoSend.IsChecked == true; _llm.DisabledTools = _disabledTools.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(); _settings.Settings.AiEnabled = true; _settings.Settings.OperationMode = OperationModePolicy.Normalize(_operationMode); _settings.Save(); SkillService.LoadSkills(_llm.SkillsFolderPath); BuildSkillListPanel(); DialogResult = true; Close(); } private void BtnBrowseSkillFolder_Click(object sender, RoutedEventArgs e) { var dlg = new System.Windows.Forms.FolderBrowserDialog { Description = "스킬 폴더 선택", ShowNewFolderButton = true, }; if (!string.IsNullOrWhiteSpace(TxtSkillsFolderPath.Text) && System.IO.Directory.Exists(TxtSkillsFolderPath.Text)) dlg.SelectedPath = TxtSkillsFolderPath.Text; if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) TxtSkillsFolderPath.Text = dlg.SelectedPath; } private void BtnOpenSkillFolder_Click(object sender, RoutedEventArgs e) { var folder = string.IsNullOrWhiteSpace(TxtSkillsFolderPath.Text) ? System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills") : TxtSkillsFolderPath.Text.Trim(); if (!System.IO.Directory.Exists(folder)) System.IO.Directory.CreateDirectory(folder); try { System.Diagnostics.Process.Start("explorer.exe", folder); } catch { } } private void BuildSkillListPanel() { if (SkillListPanel == null) return; SkillListPanel.Children.Clear(); var skills = SkillService.Skills; if (skills.Count == 0) { SkillListPanel.Children.Add(new Border { Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke, BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(10), Padding = new Thickness(12, 10, 12, 10), Child = new TextBlock { Text = "로드된 스킬이 없습니다. 스킬 폴더를 열어 `.skill.md` 또는 `SKILL.md` 파일을 추가한 뒤 저장하면 다시 불러옵니다.", FontSize = 11, TextWrapping = TextWrapping.Wrap, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, } }); return; } var groups = new[] { new { Title = "내장 스킬", Items = skills.Where(s => string.IsNullOrWhiteSpace(s.Requires)).ToList() }, new { Title = "고급 스킬", Items = skills.Where(s => !string.IsNullOrWhiteSpace(s.Requires)).ToList() }, }; foreach (var group in groups) { if (group.Items.Count == 0) continue; SkillListPanel.Children.Add(new TextBlock { Text = $"{group.Title} ({group.Items.Count})", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, Margin = new Thickness(0, 0, 0, 6), }); foreach (var skill in group.Items.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)) SkillListPanel.Children.Add(CreateSkillCard(skill)); } } private Border CreateSkillCard(SkillDefinition skill) { var available = skill.IsAvailable; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var card = new Border { Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke, BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(10), Padding = new Thickness(12, 10, 12, 10), Margin = new Thickness(0, 0, 0, 6), Opacity = available ? 1.0 : 0.72, }; static Brush HexBrush(string hex) => (Brush)new BrushConverter().ConvertFromString(hex)!; var root = new StackPanel(); var header = new DockPanel(); header.Children.Add(new TextBlock { Text = "/" + skill.Name, FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = available ? accentBrush : secondaryText, }); var badge = new Border { Background = available ? HexBrush("#ECFDF5") : HexBrush("#FEF2F2"), BorderBrush = available ? HexBrush("#BBF7D0") : HexBrush("#FECACA"), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(999), Padding = new Thickness(7, 2, 7, 2), HorizontalAlignment = HorizontalAlignment.Right, Child = new TextBlock { Text = available ? "사용 가능" : (string.IsNullOrWhiteSpace(skill.UnavailableHint) ? "사용 불가" : skill.UnavailableHint), FontSize = 10, FontWeight = FontWeights.SemiBold, Foreground = available ? HexBrush("#166534") : HexBrush("#991B1B"), } }; DockPanel.SetDock(badge, Dock.Right); header.Children.Add(badge); root.Children.Add(header); root.Children.Add(new TextBlock { Text = string.IsNullOrWhiteSpace(skill.Label) ? skill.Description : $"{skill.Label} · {skill.Description}", Margin = new Thickness(0, 4, 0, 0), FontSize = 11, TextWrapping = TextWrapping.Wrap, Foreground = secondaryText, }); if (!string.IsNullOrWhiteSpace(skill.Requires)) { root.Children.Add(new TextBlock { Text = $"필요 런타임: {skill.Requires}", Margin = new Thickness(0, 4, 0, 0), FontSize = 10.5, Foreground = secondaryText, }); } card.Child = root; return card; } private void LoadToolCards() { if (_toolCardsLoaded || ToolCardsPanel == null) return; _toolCardsLoaded = true; using var tools = ToolRegistry.CreateDefault(); var categories = new Dictionary> { ["파일/검색"] = new(), ["문서/리뷰"] = new(), ["코드/개발"] = new(), ["시스템/유틸"] = new(), }; var toolCategoryMap = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["file_read"] = "파일/검색", ["file_write"] = "파일/검색", ["file_edit"] = "파일/검색", ["glob"] = "파일/검색", ["grep"] = "파일/검색", ["document_review"] = "문서/리뷰", ["format_convert"] = "문서/리뷰", ["template_render"] = "문서/리뷰", ["text_summarize"] = "문서/리뷰", ["build_run"] = "코드/개발", ["git_tool"] = "코드/개발", ["lsp"] = "코드/개발", ["code_review"] = "코드/개발", ["test_loop"] = "코드/개발", ["process"] = "시스템/유틸", ["notify"] = "시스템/유틸", ["clipboard"] = "시스템/유틸", ["env"] = "시스템/유틸", ["skill_manager"] = "시스템/유틸", }; foreach (var tool in tools.All) { var category = toolCategoryMap.TryGetValue(tool.Name, out var mapped) ? mapped : "시스템/유틸"; categories[category].Add(tool); } foreach (var category in categories) { if (category.Value.Count == 0) continue; ToolCardsPanel.Children.Add(new TextBlock { Text = $"{category.Key} ({category.Value.Count})", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, Margin = new Thickness(0, 10, 0, 6), }); var wrap = new WrapPanel(); foreach (var tool in category.Value.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase)) wrap.Children.Add(CreateToolCard(tool)); ToolCardsPanel.Children.Add(wrap); } } private Border CreateToolCard(IAgentTool tool) { var enabled = !_disabledTools.Contains(tool.Name); var card = new Border { Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke, BorderBrush = enabled ? (TryFindResource("BorderColor") as Brush ?? Brushes.LightGray) : new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(10), Padding = new Thickness(10, 8, 10, 8), Margin = new Thickness(0, 0, 8, 8), Width = 232, }; 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(); info.Children.Add(new TextBlock { Text = tool.Name, FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, }); var desc = tool.Description.Length > 56 ? tool.Description[..56] + "…" : tool.Description; info.Children.Add(new TextBlock { Text = desc, FontSize = 10.5, Margin = new Thickness(0, 3, 0, 0), Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, TextWrapping = TextWrapping.Wrap, }); grid.Children.Add(info); var toggle = new CheckBox { IsChecked = enabled, Style = TryFindResource("ToggleSwitch") as Style, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(8, 0, 0, 0), }; toggle.Checked += (_, _) => { _disabledTools.Remove(tool.Name); card.BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray; }; toggle.Unchecked += (_, _) => { _disabledTools.Add(tool.Name); card.BorderBrush = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)); }; Grid.SetColumn(toggle, 1); grid.Children.Add(toggle); card.Child = grid; return card; } private void BtnAddHook_Click(object sender, RoutedEventArgs e) => ShowHookEditDialog(null, -1); private void ShowHookEditDialog(AgentHookEntry? existing, int index) { var dlg = new Window { Title = existing == null ? "훅 추가" : "훅 편집", Width = 420, SizeToContent = SizeToContent.Height, WindowStartupLocation = WindowStartupLocation.CenterOwner, Owner = this, ResizeMode = ResizeMode.NoResize, WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent, ShowInTaskbar = false, }; var bgBrush = TryFindResource("LauncherBackground") as Brush ?? Brushes.White; var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.Black; var subBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke; var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var root = new Border { Background = bgBrush, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(12), Padding = new Thickness(18), }; var stack = new StackPanel(); root.Child = stack; stack.Children.Add(new TextBlock { Text = existing == null ? "도구 훅 추가" : "도구 훅 편집", FontSize = 15, FontWeight = FontWeights.SemiBold, Foreground = fgBrush, }); TextBox AddField(string label, string value) { stack.Children.Add(new TextBlock { Text = label, Foreground = subBrush, FontSize = 12, Margin = new Thickness(0, 10, 0, 4) }); var box = new TextBox { Text = value, Padding = new Thickness(10, 7, 10, 7), Background = itemBg, BorderBrush = borderBrush, Foreground = fgBrush, FontSize = 12, }; stack.Children.Add(box); return box; } var nameBox = AddField("이름", existing?.Name ?? ""); var toolBox = AddField("대상 도구 (* = 전체)", existing?.ToolName ?? "*"); var pathBox = AddField("스크립트 경로", existing?.ScriptPath ?? ""); var argsBox = AddField("인수", existing?.Arguments ?? ""); var timingPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 10, 0, 0) }; var pre = new RadioButton { Content = "Pre", Foreground = fgBrush, IsChecked = (existing?.Timing ?? "post") == "pre", Margin = new Thickness(0, 0, 12, 0) }; var post = new RadioButton { Content = "Post", Foreground = fgBrush, IsChecked = (existing?.Timing ?? "post") != "pre" }; timingPanel.Children.Add(pre); timingPanel.Children.Add(post); stack.Children.Add(timingPanel); var actions = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) }; var cancel = new Button { Content = "취소", Padding = new Thickness(14, 6, 14, 6), Margin = new Thickness(0, 0, 8, 0) }; cancel.Click += (_, _) => dlg.Close(); var save = new Button { Content = "저장", Padding = new Thickness(14, 6, 14, 6), IsDefault = true }; save.Click += (_, _) => { var entry = new AgentHookEntry { Name = nameBox.Text.Trim(), ToolName = string.IsNullOrWhiteSpace(toolBox.Text) ? "*" : toolBox.Text.Trim(), Timing = pre.IsChecked == true ? "pre" : "post", ScriptPath = pathBox.Text.Trim(), Arguments = argsBox.Text.Trim(), Enabled = existing?.Enabled ?? true, }; if (index >= 0 && index < _llm.AgentHooks.Count) _llm.AgentHooks[index] = entry; else _llm.AgentHooks.Add(entry); BuildHookCards(); dlg.Close(); }; actions.Children.Add(cancel); actions.Children.Add(save); stack.Children.Add(actions); dlg.Content = root; dlg.ShowDialog(); } private void BuildHookCards() { if (HookListPanel == null) return; HookListPanel.Children.Clear(); for (var i = 0; i < _llm.AgentHooks.Count; i++) { var hook = _llm.AgentHooks[i]; var index = i; var card = new Border { Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke, BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(10), Padding = new Thickness(12, 10, 12, 10), Margin = new Thickness(0, 0, 0, 6), }; 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(); info.Children.Add(new TextBlock { Text = hook.Name, FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black }); info.Children.Add(new TextBlock { Text = $"{hook.Timing?.ToUpperInvariant() ?? "POST"} · {hook.ToolName}", FontSize = 10.5, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, Margin = new Thickness(0, 2, 0, 0) }); info.Children.Add(new TextBlock { Text = hook.ScriptPath, FontSize = 10.5, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, Margin = new Thickness(0, 2, 0, 0), TextTrimming = TextTrimming.CharacterEllipsis }); grid.Children.Add(info); var actionRow = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center }; var toggle = new CheckBox { IsChecked = hook.Enabled, Style = TryFindResource("ToggleSwitch") as Style, Margin = new Thickness(0, 0, 8, 0) }; toggle.Checked += (_, _) => hook.Enabled = true; toggle.Unchecked += (_, _) => hook.Enabled = false; var edit = new Button { Content = "편집", Style = TryFindResource("OutlineHoverBtn") as Style, Margin = new Thickness(0, 0, 6, 0) }; edit.Click += (_, _) => ShowHookEditDialog(_llm.AgentHooks[index], index); var delete = new Button { Content = "삭제", Style = TryFindResource("OutlineHoverBtn") as Style }; delete.Click += (_, _) => { _llm.AgentHooks.RemoveAt(index); BuildHookCards(); }; actionRow.Children.Add(toggle); actionRow.Children.Add(edit); actionRow.Children.Add(delete); Grid.SetColumn(actionRow, 1); grid.Children.Add(actionRow); card.Child = grid; HookListPanel.Children.Add(card); } } private void BtnAddMcpServer_Click(object sender, RoutedEventArgs e) { var nameDialog = new InputDialog("MCP 서버 추가", "서버 이름:", placeholder: "예: filesystem"); nameDialog.Owner = this; if (nameDialog.ShowDialog() != true || string.IsNullOrWhiteSpace(nameDialog.ResponseText)) return; var commandDialog = new InputDialog("MCP 서버 추가", "실행 명령:", placeholder: "예: npx -y @modelcontextprotocol/server-filesystem"); commandDialog.Owner = this; if (commandDialog.ShowDialog() != true || string.IsNullOrWhiteSpace(commandDialog.ResponseText)) return; _llm.McpServers.Add(new McpServerEntry { Name = nameDialog.ResponseText.Trim(), Command = commandDialog.ResponseText.Trim(), Enabled = true, }); BuildMcpServerCards(); } private void BuildMcpServerCards() { if (McpServerListPanel == null) return; McpServerListPanel.Children.Clear(); foreach (var entry in _llm.McpServers.ToList()) { var card = new Border { Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke, BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(10), Padding = new Thickness(12, 10, 12, 10), Margin = new Thickness(0, 0, 0, 6), }; 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(); info.Children.Add(new TextBlock { Text = entry.Name, FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black }); info.Children.Add(new TextBlock { Text = entry.Command, FontSize = 10.5, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, Margin = new Thickness(0, 2, 0, 0), TextTrimming = TextTrimming.CharacterEllipsis }); grid.Children.Add(info); var actionRow = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center }; var toggle = new CheckBox { IsChecked = entry.Enabled, Style = TryFindResource("ToggleSwitch") as Style, Margin = new Thickness(0, 0, 8, 0) }; toggle.Checked += (_, _) => entry.Enabled = true; toggle.Unchecked += (_, _) => entry.Enabled = false; var delete = new Button { Content = "삭제", Style = TryFindResource("OutlineHoverBtn") as Style }; delete.Click += (_, _) => { _llm.McpServers.Remove(entry); BuildMcpServerCards(); }; actionRow.Children.Add(toggle); actionRow.Children.Add(delete); Grid.SetColumn(actionRow, 1); grid.Children.Add(actionRow); card.Child = grid; McpServerListPanel.Children.Add(card); } } private void BuildFallbackModelsPanel() { if (FallbackModelsPanel == null) return; FallbackModelsPanel.Children.Clear(); var sections = new (string Service, string Label, List Models)[] { ("ollama", "Ollama", GetModelCandidates("ollama")), ("vllm", "vLLM", GetModelCandidates("vllm")), ("gemini", "Gemini", new[] { _llm.GeminiModel }.Where(x => !string.IsNullOrWhiteSpace(x)).ToList()!), ("claude", "Claude", new[] { _llm.ClaudeModel }.Where(x => !string.IsNullOrWhiteSpace(x)).ToList()!), }; foreach (var section in sections) { FallbackModelsPanel.Children.Add(new TextBlock { Text = section.Label, FontSize = 11.5, FontWeight = FontWeights.SemiBold, Margin = new Thickness(0, 8, 0, 4), Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, }); var models = section.Models.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); if (models.Count == 0) { FallbackModelsPanel.Children.Add(new TextBlock { Text = "등록된 모델 없음", FontSize = 10.5, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, Margin = new Thickness(8, 0, 0, 4), }); continue; } foreach (var model in models) { var key = $"{section.Service}:{model}"; 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 }); row.Children.Add(new TextBlock { Text = model, FontSize = 11.5, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, VerticalAlignment = VerticalAlignment.Center, }); var toggle = new CheckBox { IsChecked = _llm.FallbackModels.Contains(key, StringComparer.OrdinalIgnoreCase), Style = TryFindResource("ToggleSwitch") as Style, VerticalAlignment = VerticalAlignment.Center, }; toggle.Checked += (_, _) => { if (!_llm.FallbackModels.Contains(key, StringComparer.OrdinalIgnoreCase)) _llm.FallbackModels.Add(key); }; toggle.Unchecked += (_, _) => _llm.FallbackModels.RemoveAll(x => x.Equals(key, StringComparison.OrdinalIgnoreCase)); Grid.SetColumn(toggle, 1); row.Children.Add(toggle); FallbackModelsPanel.Children.Add(row); } } } private void BtnOpenFullSettings_Click(object sender, RoutedEventArgs e) { if (System.Windows.Application.Current is App app) app.OpenSettingsFromChat(); } private void BtnClose_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => Close(); private static int ParseInt(string? text, int fallback, int min, int max) { if (!int.TryParse(text, out var value)) value = fallback; return Math.Clamp(value, min, max); } }