using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using AxCopilot.Models; using AxCopilot.Services; using AxCopilot.Services.Agent; namespace AxCopilot.Views; public partial class AgentSettingsWindow : Window { private const string UnifiedAdminPassword = "axgo123!"; private readonly SettingsService _settings; private readonly LlmSettings _llm; private string _permissionMode = PermissionModeCatalog.Deny; private string _planMode = "off"; private string _reasoningMode = "detailed"; private string _folderDataUsage = "active"; private string _operationMode = OperationModePolicy.InternalMode; private string _selectedModel = string.Empty; public AgentSettingsWindow(SettingsService settings) { _settings = settings; _llm = _settings.Settings.Llm; InitializeComponent(); LoadFromSettings(); } private void LoadFromSettings() { _selectedModel = _llm.Model ?? ""; ModelInput.Text = _selectedModel; _permissionMode = PermissionModeCatalog.NormalizeGlobalMode(_llm.FilePermission); _planMode = string.IsNullOrWhiteSpace(_llm.PlanMode) ? "off" : _llm.PlanMode; _reasoningMode = string.IsNullOrWhiteSpace(_llm.AgentDecisionLevel) ? "detailed" : _llm.AgentDecisionLevel; _folderDataUsage = string.IsNullOrWhiteSpace(_llm.FolderDataUsage) ? "active" : _llm.FolderDataUsage; _operationMode = OperationModePolicy.Normalize(_settings.Settings.OperationMode); 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; ChkEnableCodeVerification.IsChecked = _llm.Code.EnableCodeVerification; ChkEnableParallelTools.IsChecked = _llm.EnableParallelTools; RefreshServiceCards(); RefreshThemeCards(); RefreshModeLabels(); BuildModelChips(); } private void RefreshThemeCards() { var selected = (_llm.AgentTheme ?? "system").ToLowerInvariant(); SetCardSelection(ThemeSystemCard, selected == "system"); SetCardSelection(ThemeLightCard, selected == "light"); SetCardSelection(ThemeDarkCard, selected == "dark"); } private void RefreshServiceCards() { var service = (_llm.Service ?? "ollama").ToLowerInvariant(); SetCardSelection(SvcOllamaCard, service == "ollama"); SetCardSelection(SvcVllmCard, service == "vllm"); SetCardSelection(SvcGeminiCard, service == "gemini"); SetCardSelection(SvcClaudeCard, service is "claude" or "sigmoid"); } private void RefreshModeLabels() { BtnOperationMode.Content = BuildOperationModeLabel(_operationMode); BtnPermissionMode.Content = PermissionModeCatalog.ToDisplayLabel(_permissionMode); BtnPlanMode.Content = BuildPlanModeLabel(_planMode); BtnReasoningMode.Content = BuildReasoningModeLabel(_reasoningMode); BtnFolderDataUsage.Content = BuildFolderDataUsageLabel(_folderDataUsage); AdvancedPanel.Visibility = Visibility.Visible; } private static string BuildOperationModeLabel(string mode) { return OperationModePolicy.Normalize(mode) == OperationModePolicy.ExternalMode ? "사외 모드" : "사내 모드"; } private static string BuildPlanModeLabel(string mode) { return (mode ?? "off").ToLowerInvariant() switch { "always" => "항상 계획", "auto" => "자동 계획", _ => "끄기", }; } private static string BuildReasoningModeLabel(string mode) { return (mode ?? "detailed").ToLowerInvariant() switch { "minimal" => "낮음", "normal" => "중간", _ => "높음", }; } private static string BuildFolderDataUsageLabel(string mode) { return (mode ?? "none").ToLowerInvariant() switch { "active" => "적극 활용", "passive" => "소극 활용", _ => "활용하지 않음", }; } 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(_llm.Service); 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) { _llm.Service = service; RefreshServiceCards(); BuildModelChips(); } private void SetTheme(string theme) { _llm.AgentTheme = theme; RefreshThemeCards(); } 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 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 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 BtnPlanMode_Click(object sender, RoutedEventArgs e) { _planMode = _planMode switch { "off" => "auto", "auto" => "always", _ => "off", }; RefreshModeLabels(); } private void BtnReasoningMode_Click(object sender, RoutedEventArgs e) { _reasoningMode = _reasoningMode switch { "minimal" => "normal", "normal" => "detailed", _ => "minimal", }; RefreshModeLabels(); } private void BtnFolderDataUsage_Click(object sender, RoutedEventArgs e) { _folderDataUsage = _folderDataUsage switch { "none" => "passive", "passive" => "active", _ => "none", }; RefreshModeLabels(); } private void BtnSave_Click(object sender, RoutedEventArgs e) { _llm.Model = string.IsNullOrWhiteSpace(_selectedModel) ? (_llm.Model ?? "") : _selectedModel.Trim(); _llm.FilePermission = _permissionMode; _llm.PlanMode = _planMode; _llm.AgentDecisionLevel = _reasoningMode; _llm.FolderDataUsage = _folderDataUsage; _llm.AgentUiExpressionLevel = "rich"; _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.Code.EnableCodeVerification = ChkEnableCodeVerification.IsChecked == true; _llm.EnableParallelTools = ChkEnableParallelTools.IsChecked == true; _settings.Settings.OperationMode = OperationModePolicy.Normalize(_operationMode); _settings.Save(); DialogResult = true; Close(); } 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); } }