using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; using Microsoft.Win32; using AxCopilot.Models; using AxCopilot.Services; using AxCopilot.Services.Agent; namespace AxCopilot.Views; public partial class ChatWindow { // ─── 모델 전환 ────────────────────────────────────────────────────── // Gemini/Claude 사전 정의 모델 목록 private static readonly (string Id, string Label)[] GeminiModels = { ("gemini-2.5-pro", "Gemini 2.5 Pro"), ("gemini-2.5-flash", "Gemini 2.5 Flash"), ("gemini-2.5-flash-lite", "Gemini 2.5 Flash Lite"), ("gemini-2.0-flash", "Gemini 2.0 Flash"), ("gemini-2.0-flash-lite", "Gemini 2.0 Flash Lite"), }; private static readonly (string Id, string Label)[] ClaudeModels = { (string.Concat("cl", "aude-opus-4-6"), "Claude Opus 4.6"), (string.Concat("cl", "aude-sonnet-4-6"), "Claude Sonnet 4.6"), (string.Concat("cl", "aude-haiku-4-5-20251001"), "Claude Haiku 4.5"), (string.Concat("cl", "aude-sonnet-4-5-20250929"), "Claude Sonnet 4.5"), (string.Concat("cl", "aude-opus-4-20250514"), "Claude Opus 4"), }; /// 현재 선택된 모델의 표시명을 반환합니다. private string GetCurrentModelDisplayName() { var llm = _settings.Settings.Llm; var service = llm.Service.ToLowerInvariant(); if (service is "ollama" or "vllm") { // 등록 모델에서 별칭 찾기 var registered = llm.RegisteredModels .FirstOrDefault(rm => rm.EncryptedModelName == llm.Model); if (registered != null) return registered.Alias; return string.IsNullOrEmpty(llm.Model) ? "(미설정)" : "••••"; } if (service == "gemini") { var m = GeminiModels.FirstOrDefault(g => g.Id == llm.Model); return m.Label ?? llm.Model; } if (service is "sigmoid" or "cl" + "aude") { var m = ClaudeModels.FirstOrDefault(c => c.Id == llm.Model); return m.Label ?? llm.Model; } return string.IsNullOrEmpty(llm.Model) ? "(미설정)" : llm.Model; } private void UpdateModelLabel() { var service = _settings.Settings.Llm.Service.ToLowerInvariant(); var serviceLabel = service switch { "gemini" => "Gemini", "sigmoid" or "cl" + "aude" => "Claude", "vllm" => "vLLM", _ => "Ollama", }; var model = GetCurrentModelDisplayName(); const int maxLen = 24; if (model.Length > maxLen) model = model[..(maxLen - 1)] + "…"; ModelLabel.Text = string.IsNullOrWhiteSpace(model) ? serviceLabel : model; if (BtnModelSelector != null) BtnModelSelector.ToolTip = $"현재 모델: {GetCurrentModelDisplayName()}\n서비스: {serviceLabel}"; } private static string NextReasoning(string current) => (current ?? "normal").ToLowerInvariant() switch { "minimal" => "normal", "normal" => "detailed", _ => "minimal", }; private static string ReasoningLabel(string value) => (value ?? "normal").ToLowerInvariant() switch { "minimal" => "낮음", "detailed" => "높음", _ => "중간", }; private static string NextPermission(string current) => PermissionModeCatalog.NormalizeGlobalMode(current).ToLowerInvariant() switch { "deny" => "Default", "default" => "AcceptEdits", "acceptedits" => "Plan", "plan" => "BypassPermissions", "bypasspermissions" => "Default", _ => "Default", }; private static string ServiceLabel(string service) => (service ?? "").ToLowerInvariant() switch { "gemini" => "Gemini", "sigmoid" or "cl" + "aude" => "Claude", "vllm" => "vLLM", _ => "Ollama", }; private List<(string Id, string Label)> GetModelCandidates(string service) { var llm = _settings.Settings.Llm; var normalized = (service ?? "ollama").ToLowerInvariant(); if (normalized is "ollama" or "vllm") { return llm.RegisteredModels .Where(rm => string.Equals(rm.Service, normalized, StringComparison.OrdinalIgnoreCase)) .Select(rm => (rm.EncryptedModelName, rm.Alias)) .ToList(); } if (normalized == "gemini") return GeminiModels.Select(m => (m.Id, m.Label)).ToList(); if (normalized is "sigmoid" or "cl" + "aude") return ClaudeModels.Select(m => (m.Id, m.Label)).ToList(); return []; } private static bool SupportsOverlayRegisteredModels(string service) => string.Equals(service, "ollama", StringComparison.OrdinalIgnoreCase) || string.Equals(service, "vllm", StringComparison.OrdinalIgnoreCase); private void RefreshInlineSettingsPanel() { if (InlineSettingsPanel == null) return; var llm = _settings.Settings.Llm; var service = (llm.Service ?? "ollama").ToLowerInvariant(); var models = GetModelCandidates(service); _isInlineSettingsSyncing = true; try { if (InlineServiceCardPanel != null) InlineServiceCardPanel.Children.Clear(); if (InlineModelListPanel != null) InlineModelListPanel.Children.Clear(); CmbInlineService.Items.Clear(); foreach (var svc in new[] { "ollama", "vllm", "gemini", "claude" }) { CmbInlineService.Items.Add(new ComboBoxItem { Content = ServiceLabel(svc), Tag = svc, }); } var normalizedService = service == "sigmoid" ? string.Concat("cl", "aude") : service; CmbInlineService.SelectedIndex = Math.Max(0, new[] { "ollama", "vllm", "gemini", "claude" }.ToList().IndexOf(normalizedService)); BuildInlineServiceCards(normalizedService); CmbInlineModel.Items.Clear(); if (models.Count == 0) { CmbInlineModel.Items.Add(new ComboBoxItem { Content = "등록된 모델 없음", IsEnabled = false }); CmbInlineModel.SelectedIndex = 0; } else { foreach (var (id, label) in models) { CmbInlineModel.Items.Add(new ComboBoxItem { Content = label, Tag = id, }); } var selectedIndex = models.FindIndex(m => m.Id == llm.Model); CmbInlineModel.SelectedIndex = selectedIndex >= 0 ? selectedIndex : 0; BuildInlineModelRows(models, llm.Model); } BtnInlineFastMode.Content = GetQuickActionLabel("Gemini 대기", llm.FreeTierMode ? "켜짐" : "꺼짐"); BtnInlineReasoning.Content = GetQuickActionLabel("추론", ReasoningLabel(llm.AgentDecisionLevel)); BtnInlinePermission.Content = GetQuickActionLabel("권한", PermissionModeCatalog.ToDisplayLabel(llm.FilePermission)); BtnInlineSkill.Content = $"스킬 · {(llm.EnableSkillSystem ? "On" : "Off")}"; BtnInlineCommandBrowser.Content = "명령/스킬 브라우저"; var mcpTotal = llm.McpServers?.Count ?? 0; var mcpEnabled = llm.McpServers?.Count(x => x.Enabled) ?? 0; BtnInlineMcp.Content = $"MCP 상태 · {mcpEnabled}/{mcpTotal}"; ApplyQuickActionVisual(BtnInlineFastMode, llm.FreeTierMode, "#ECFDF5", "#166534"); ApplyQuickActionVisual(BtnInlineReasoning, !string.Equals(llm.AgentDecisionLevel, "normal", StringComparison.OrdinalIgnoreCase), "#EEF2FF", "#1D4ED8"); ApplyQuickActionVisual(BtnInlinePermission, !string.Equals(PermissionModeCatalog.NormalizeGlobalMode(llm.FilePermission), PermissionModeCatalog.Deny, StringComparison.OrdinalIgnoreCase), "#FFF7ED", "#C2410C"); } finally { _isInlineSettingsSyncing = false; } } private void BuildInlineServiceCards(string selectedService) { if (InlineServiceCardPanel == null) return; // 테마 색상 미리 로드 — 하드코딩된 라이트 전용 색상 대신 테마 리소스 사용 var hintBg = TryFindResource("HintBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0xEA, 0xEA, 0xF0)); var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var borderColor = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? hintBg; foreach (var svc in new[] { "ollama", "vllm", "gemini", "claude" }) { var isActive = string.Equals(svc, selectedService, StringComparison.OrdinalIgnoreCase); var card = new Border { Background = isActive ? hintBg : Brushes.Transparent, BorderBrush = isActive ? accentBrush : borderColor, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(14), Padding = new Thickness(10, 6, 10, 6), Margin = new Thickness(0, 0, 6, 6), Cursor = Cursors.Hand, }; var text = new TextBlock { Text = ServiceLabel(svc), FontSize = 10.5, Foreground = isActive ? accentBrush : primaryText, }; card.Child = text; var capturedService = svc; card.MouseEnter += (_, _) => { if (!isActive) card.Background = hoverBg; }; card.MouseLeave += (_, _) => { if (!isActive) card.Background = Brushes.Transparent; }; card.MouseLeftButtonUp += (_, _) => { if (_isInlineSettingsSyncing) return; _settings.Settings.Llm.Service = capturedService; ScheduleSettingsSave(); UpdateModelLabel(); RefreshInlineSettingsPanel(); }; InlineServiceCardPanel.Children.Add(card); } } private void BuildInlineModelRows(List<(string Id, string Label)> models, string? selectedModel) { if (InlineModelListPanel == null) return; // 테마 색상 미리 로드 — 하드코딩된 라이트 전용 색상 대신 테마 리소스 사용 // (다크 테마에서 흰 배경 + 흰 텍스트로 글자가 안보이는 문제 수정) var hintBg = TryFindResource("HintBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0xEA, 0xEA, 0xF0)); var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var borderColor = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; foreach (var (id, label) in models.Take(8)) { var isActive = string.Equals(id, selectedModel, StringComparison.OrdinalIgnoreCase); var row = new Border { Background = isActive ? hintBg : Brushes.Transparent, BorderBrush = isActive ? accentBrush : borderColor, BorderThickness = new Thickness(isActive ? 2 : 0, 0, 0, 1), Padding = new Thickness(8, 8, 8, 8), Cursor = Cursors.Hand, }; var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var labelText = new TextBlock { Text = label, FontSize = 11.5, FontWeight = FontWeights.SemiBold, Foreground = primaryText, }; grid.Children.Add(labelText); var stateText = new TextBlock { Text = isActive ? "사용 중" : "선택", FontSize = 10, Foreground = isActive ? accentBrush : secondaryText, VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(stateText, 1); grid.Children.Add(stateText); row.Child = grid; var capturedId = id; var capturedLabel = label; row.MouseEnter += (_, _) => { row.Background = hintBg; row.BorderBrush = isActive ? accentBrush : borderColor; }; row.MouseLeave += (_, _) => { row.Background = isActive ? hintBg : Brushes.Transparent; row.BorderBrush = isActive ? accentBrush : borderColor; }; row.MouseLeftButtonUp += (_, _) => { if (_isInlineSettingsSyncing) return; _settings.Settings.Llm.Model = capturedId; ScheduleSettingsSave(); UpdateModelLabel(); RefreshInlineSettingsPanel(); SetStatus($"모델 전환: {capturedLabel}", spinning: false); }; InlineModelListPanel.Children.Add(row); } } private void BtnModelSelector_Click(object sender, RoutedEventArgs e) { RefreshInlineSettingsPanel(); InlineSettingsPanel.IsOpen = !InlineSettingsPanel.IsOpen; if (InlineSettingsPanel.IsOpen) { Dispatcher.BeginInvoke(() => { if (CmbInlineModel.Items.Count > 0) CmbInlineModel.Focus(); else InputBox.Focus(); }, DispatcherPriority.Input); } } private void OpenAgentSettingsWindow() { RefreshOverlaySettingsPanel(); // Scale + fade-in animation AgentSettingsOverlay.RenderTransform = new ScaleTransform(0.98, 0.98); AgentSettingsOverlay.RenderTransformOrigin = new Point(0.5, 0.5); AgentSettingsOverlay.Opacity = 0; AgentSettingsOverlay.Visibility = Visibility.Visible; var ease = new System.Windows.Media.Animation.CubicEase(); var scaleXAnim = new System.Windows.Media.Animation.DoubleAnimation(0.98, 1.0, TimeSpan.FromMilliseconds(200)) { EasingFunction = ease }; var scaleYAnim = new System.Windows.Media.Animation.DoubleAnimation(0.98, 1.0, TimeSpan.FromMilliseconds(200)) { EasingFunction = ease }; var fadeAnim = new System.Windows.Media.Animation.DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(180)); ((ScaleTransform)AgentSettingsOverlay.RenderTransform).BeginAnimation(ScaleTransform.ScaleXProperty, scaleXAnim); ((ScaleTransform)AgentSettingsOverlay.RenderTransform).BeginAnimation(ScaleTransform.ScaleYProperty, scaleYAnim); AgentSettingsOverlay.BeginAnimation(UIElement.OpacityProperty, fadeAnim); InlineSettingsPanel.IsOpen = false; // 탭 RadioButton을 "공통"으로 확실히 리셋 — 재진입 시 탭/패널 불일치 방지 if (OverlayNavBasic != null) OverlayNavBasic.IsChecked = true; SetOverlaySection("basic"); Dispatcher.BeginInvoke(() => { if (OverlayNavBasic != null) OverlayNavBasic.Focus(); else InputBox.Focus(); }, DispatcherPriority.Input); } public void OpenAgentSettingsFromExternal() { Dispatcher.BeginInvoke(() => { Show(); Activate(); OpenAgentSettingsWindow(); }, DispatcherPriority.Input); } public void RefreshFromSavedSettings() { Dispatcher.BeginInvoke(() => { ApplyAgentThemeResources(); LoadConversationSettings(); UpdatePermissionUI(); UpdateDataUsageUI(); UpdateModelLabel(); RefreshInlineSettingsPanel(); RefreshOverlaySettingsPanel(); RefreshContextUsageVisual(); UpdateTabUI(); BuildBottomBar(); RefreshDraftQueueUi(); RefreshConversationList(); if (_isStreaming && _settings.Settings.Llm.EnableChatRainbowGlow) PlayRainbowGlow(); else StopRainbowGlow(); }, DispatcherPriority.Input); } private void BtnOverlaySettingsClose_Click(object sender, RoutedEventArgs e) { ApplyOverlaySettingsChanges(showToast: false, closeOverlay: true); } private void ApplyOverlaySettingsChanges(bool showToast, bool closeOverlay) { // InitializeComponent() 도중 ComboBox IsSelected="True"가 SelectionChanged를 발동시킬 수 있음 // 이 시점에 _settings가 아직 null이므로 무시 if (_settings == null) return; var llm = _settings.Settings.Llm; _settings.Settings.AiEnabled = true; llm.EnableProactiveContextCompact = ChkOverlayEnableProactiveCompact?.IsChecked == true; llm.EnableSkillSystem = ChkOverlayEnableSkillSystem?.IsChecked == true; llm.EnableToolHooks = ChkOverlayEnableToolHooks?.IsChecked == true; llm.EnableHookInputMutation = ChkOverlayEnableHookInputMutation?.IsChecked == true; llm.EnableHookPermissionUpdate = ChkOverlayEnableHookPermissionUpdate?.IsChecked == true; llm.EnableCoworkVerification = ChkOverlayEnableCoworkVerification?.IsChecked == true; llm.CoworkOnComplete = (CmbOverlayCoworkOnComplete?.SelectedItem as System.Windows.Controls.ComboBoxItem)?.Tag?.ToString() ?? "none"; llm.AutoPreview = (CmbOverlayAutoPreview?.SelectedItem as System.Windows.Controls.ComboBoxItem)?.Tag?.ToString() ?? "off"; llm.Code.EnableCodeVerification = ChkOverlayEnableCodeVerification?.IsChecked == true; llm.Code.EnableCodeReview = ChkOverlayEnableCodeReview?.IsChecked == true; llm.EnableImageInput = ChkOverlayEnableImageInput?.IsChecked == true; llm.EnableParallelTools = ChkOverlayEnableParallelTools?.IsChecked == true; llm.EnableProjectRules = ChkOverlayEnableProjectRules?.IsChecked == true; llm.EnableAgentMemory = ChkOverlayEnableAgentMemory?.IsChecked == true; llm.EnableIbmDiagnosticLog = ChkOverlayEnableIbmDiagnosticLog?.IsChecked == true; llm.Code.EnableWorktreeTools = ChkOverlayEnableWorktreeTools?.IsChecked == true; llm.Code.EnableTeamTools = ChkOverlayEnableTeamTools?.IsChecked == true; llm.Code.EnableCronTools = ChkOverlayEnableCronTools?.IsChecked == true; llm.Code.MascotLevel = (CmbOverlayMascotLevel?.SelectedItem as System.Windows.Controls.ComboBoxItem)?.Tag?.ToString() ?? "none"; llm.WorkflowVisualizer = ChkOverlayWorkflowVisualizer?.IsChecked == true; llm.ShowTotalCallStats = ChkOverlayShowTotalCallStats?.IsChecked == true; llm.EnableAuditLog = ChkOverlayEnableAuditLog?.IsChecked == true; llm.EnableChatRainbowGlow = ChkOverlayEnableChatRainbowGlow?.IsChecked == true; _settings.Settings.Launcher.EnableChatIconRandomAnimation = ChkOverlayEnableChatIconRandomAnim?.IsChecked == true; _settings.Settings.Launcher.ChatIconGlowIntensity = (CmbOverlayChatIconGlow?.SelectedItem as System.Windows.Controls.ComboBoxItem)?.Tag?.ToString() ?? "medium"; // V2 뷰어/렌더링 전환 완료 — 항상 V2 사용 CommitOverlayEndpointInput(normalizeOnInvalid: true); CommitOverlayApiKeyInput(); CommitOverlayModelInput(normalizeOnInvalid: true); CommitOverlayNumericInput(TxtOverlayContextCompactTriggerPercent, llm.ContextCompactTriggerPercent, 10, 95, value => llm.ContextCompactTriggerPercent = value, normalizeOnInvalid: true); CommitOverlayNumericInput(TxtOverlayMaxContextTokens, llm.MaxContextTokens, 1024, 1_000_000, value => llm.MaxContextTokens = value, normalizeOnInvalid: true); CommitOverlayTemperatureInput(normalizeOnInvalid: true); CommitOverlayNumericInput(TxtOverlayMaxRetryOnError, llm.MaxRetryOnError, 0, 10, value => llm.MaxRetryOnError = value, normalizeOnInvalid: true); CommitOverlayNumericInput(TxtOverlayMaxAgentIterations, llm.MaxAgentIterations, 1, 200, value => llm.MaxAgentIterations = value, normalizeOnInvalid: true); CommitOverlayNumericInput(TxtOverlayFreeTierDelaySeconds, llm.FreeTierDelaySeconds, 0, 60, value => llm.FreeTierDelaySeconds = value, normalizeOnInvalid: true); CommitOverlayNumericInput(TxtOverlayMaxSubAgents, llm.MaxSubAgents, 1, 10, value => llm.MaxSubAgents = value, normalizeOnInvalid: true); CommitOverlayNumericInput(TxtOverlayToolHookTimeoutMs, llm.ToolHookTimeoutMs, 3000, 30000, value => llm.ToolHookTimeoutMs = value, normalizeOnInvalid: true); CommitOverlayNumericInput(TxtOverlayMaxFavoriteSlashCommands, llm.MaxFavoriteSlashCommands, 1, 30, value => llm.MaxFavoriteSlashCommands = value, normalizeOnInvalid: true); CommitOverlayNumericInput(TxtOverlayMaxRecentSlashCommands, llm.MaxRecentSlashCommands, 5, 50, value => llm.MaxRecentSlashCommands = value, normalizeOnInvalid: true); CommitOverlayNumericInput(TxtOverlayPlanDiffMediumCount, llm.PlanDiffSeverityMediumCount, 1, 999, value => llm.PlanDiffSeverityMediumCount = value, normalizeOnInvalid: true); CommitOverlayNumericInput(TxtOverlayPlanDiffHighCount, llm.PlanDiffSeverityHighCount, 1, 999, value => llm.PlanDiffSeverityHighCount = value, normalizeOnInvalid: true); CommitOverlayNumericInput(TxtOverlayPlanDiffMediumRatio, llm.PlanDiffSeverityMediumRatioPercent, 1, 100, value => llm.PlanDiffSeverityMediumRatioPercent = value, normalizeOnInvalid: true); CommitOverlayNumericInput(TxtOverlayPlanDiffHighRatio, llm.PlanDiffSeverityHighRatioPercent, 1, 100, value => llm.PlanDiffSeverityHighRatioPercent = value, normalizeOnInvalid: true); if (TxtOverlayPdfExportPath != null) llm.PdfExportPath = TxtOverlayPdfExportPath.Text.Trim(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); if (closeOverlay) { var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(120)); fadeOut.Completed += (_, _) => { AgentSettingsOverlay.Visibility = Visibility.Collapsed; AgentSettingsOverlay.BeginAnimation(UIElement.OpacityProperty, null); AgentSettingsOverlay.Opacity = 1; }; AgentSettingsOverlay.BeginAnimation(UIElement.OpacityProperty, fadeOut); } if (showToast) ShowToast("AX Agent 설정이 저장되었습니다."); InputBox.Focus(); } private void PersistOverlaySettingsState(bool refreshOverlayDeferredInputs) { ScheduleSettingsSave(); _appState.LoadFromSettings(_settings); ApplyAgentThemeResources(); UpdatePermissionUI(); UpdateDataUsageUI(); SyncWorkflowVisualizerWindow(); SaveConversationSettings(); RefreshInlineSettingsPanel(); UpdateModelLabel(); UpdateTabUI(); RefreshOverlayVisualState(refreshOverlayDeferredInputs); } private void RefreshOverlayVisualState(bool loadDeferredInputs) { var llm = _settings.Settings.Llm; var service = NormalizeOverlayService(llm.Service); var models = GetModelCandidates(service); _isOverlaySettingsSyncing = true; try { if (CmbOverlayService != null) { CmbOverlayService.Items.Clear(); foreach (var svc in new[] { "ollama", "vllm", "gemini", "claude" }) { CmbOverlayService.Items.Add(new ComboBoxItem { Content = ServiceLabel(svc), Tag = svc }); } CmbOverlayService.SelectedItem = CmbOverlayService.Items .OfType() .FirstOrDefault(i => string.Equals(i.Tag as string, service, StringComparison.OrdinalIgnoreCase)); } if (CmbOverlayModel != null) { CmbOverlayModel.Items.Clear(); foreach (var model in models) { CmbOverlayModel.Items.Add(new ComboBoxItem { Content = model.Label, Tag = model.Id }); } CmbOverlayModel.SelectedItem = CmbOverlayModel.Items .OfType() .FirstOrDefault(i => string.Equals(i.Tag as string, llm.Model, StringComparison.OrdinalIgnoreCase)); if (CmbOverlayModel.SelectedItem == null && CmbOverlayModel.Items.Count > 0) CmbOverlayModel.SelectedIndex = 0; } var serviceText = ServiceLabel(service); var modelText = string.IsNullOrWhiteSpace(llm.Model) ? "미선택" : (models.FirstOrDefault(m => string.Equals(m.Id, llm.Model, StringComparison.OrdinalIgnoreCase)).Label ?? llm.Model); ViewModel.ServiceLabel = serviceText; ViewModel.ModelLabel = modelText; PopulateOverlayMoodCombo(); if (loadDeferredInputs) { if (ChkOverlayAiEnabled != null) ChkOverlayAiEnabled.IsChecked = true; if (TxtOverlayServiceEndpoint != null) TxtOverlayServiceEndpoint.Text = GetOverlayServiceEndpoint(service); if (TxtOverlayServiceApiKey != null) TxtOverlayServiceApiKey.Text = GetOverlayServiceApiKey(service); if (TxtOverlayContextCompactTriggerPercent != null) TxtOverlayContextCompactTriggerPercent.Text = Math.Clamp(llm.ContextCompactTriggerPercent, 10, 95).ToString(); if (TxtOverlayMaxContextTokens != null) TxtOverlayMaxContextTokens.Text = Math.Clamp(llm.MaxContextTokens, 1024, 1_000_000).ToString(); if (TxtOverlayTemperature != null) TxtOverlayTemperature.Text = Math.Round(Math.Clamp(llm.Temperature, 0.0, 2.0), 1).ToString("0.0"); if (SldOverlayTemperature != null) SldOverlayTemperature.Value = Math.Round(Math.Clamp(llm.Temperature, 0.0, 2.0), 1); if (TxtOverlayTemperatureValue != null) TxtOverlayTemperatureValue.Text = Math.Round(Math.Clamp(llm.Temperature, 0.0, 2.0), 1).ToString("0.0"); RefreshOverlayTemperatureModeButtons(); if (TxtOverlayMaxRetryOnError != null) TxtOverlayMaxRetryOnError.Text = Math.Clamp(llm.MaxRetryOnError, 0, 10).ToString(); if (SldOverlayMaxRetryOnError != null) SldOverlayMaxRetryOnError.Value = Math.Clamp(llm.MaxRetryOnError, 0, 10); if (TxtOverlayMaxRetryOnErrorValue != null) TxtOverlayMaxRetryOnErrorValue.Text = Math.Clamp(llm.MaxRetryOnError, 0, 10).ToString(); if (TxtOverlayMaxAgentIterations != null) TxtOverlayMaxAgentIterations.Text = Math.Clamp(llm.MaxAgentIterations, 1, 200).ToString(); if (SldOverlayMaxAgentIterations != null) SldOverlayMaxAgentIterations.Value = Math.Clamp(llm.MaxAgentIterations, 1, 100); if (TxtOverlayMaxAgentIterationsValue != null) TxtOverlayMaxAgentIterationsValue.Text = Math.Clamp(llm.MaxAgentIterations, 1, 100).ToString(); if (TxtOverlayFreeTierDelaySeconds != null) TxtOverlayFreeTierDelaySeconds.Text = Math.Clamp(llm.FreeTierDelaySeconds, 0, 60).ToString(); if (SldOverlayFreeTierDelaySeconds != null) SldOverlayFreeTierDelaySeconds.Value = Math.Clamp(llm.FreeTierDelaySeconds, 0, 60); if (TxtOverlayFreeTierDelaySecondsValue != null) TxtOverlayFreeTierDelaySecondsValue.Text = Math.Clamp(llm.FreeTierDelaySeconds, 0, 60).ToString(); if (TxtOverlayMaxSubAgents != null) TxtOverlayMaxSubAgents.Text = Math.Clamp(llm.MaxSubAgents, 1, 10).ToString(); if (SldOverlayMaxSubAgents != null) SldOverlayMaxSubAgents.Value = Math.Clamp(llm.MaxSubAgents, 1, 10); if (TxtOverlayMaxSubAgentsValue != null) TxtOverlayMaxSubAgentsValue.Text = Math.Clamp(llm.MaxSubAgents, 1, 10).ToString(); if (TxtOverlayToolHookTimeoutMs != null) TxtOverlayToolHookTimeoutMs.Text = Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000).ToString(); if (SldOverlayToolHookTimeoutMs != null) SldOverlayToolHookTimeoutMs.Value = Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000); if (TxtOverlayToolHookTimeoutMsValue != null) TxtOverlayToolHookTimeoutMsValue.Text = $"{Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000) / 1000}s"; if (TxtOverlayMaxFavoriteSlashCommands != null) TxtOverlayMaxFavoriteSlashCommands.Text = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30).ToString(); if (SldOverlayMaxFavoriteSlashCommands != null) SldOverlayMaxFavoriteSlashCommands.Value = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30); if (TxtOverlayMaxFavoriteSlashCommandsValue != null) TxtOverlayMaxFavoriteSlashCommandsValue.Text = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30).ToString(); if (TxtOverlayMaxRecentSlashCommands != null) TxtOverlayMaxRecentSlashCommands.Text = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50).ToString(); if (SldOverlayMaxRecentSlashCommands != null) SldOverlayMaxRecentSlashCommands.Value = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50); if (TxtOverlayMaxRecentSlashCommandsValue != null) TxtOverlayMaxRecentSlashCommandsValue.Text = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50).ToString(); if (TxtOverlayPlanDiffMediumCount != null) TxtOverlayPlanDiffMediumCount.Text = Math.Clamp(llm.PlanDiffSeverityMediumCount, 1, 999).ToString(); if (TxtOverlayPlanDiffHighCount != null) TxtOverlayPlanDiffHighCount.Text = Math.Clamp(llm.PlanDiffSeverityHighCount, 1, 999).ToString(); if (TxtOverlayPlanDiffMediumRatio != null) TxtOverlayPlanDiffMediumRatio.Text = Math.Clamp(llm.PlanDiffSeverityMediumRatioPercent, 1, 100).ToString(); if (TxtOverlayPlanDiffHighRatio != null) TxtOverlayPlanDiffHighRatio.Text = Math.Clamp(llm.PlanDiffSeverityHighRatioPercent, 1, 100).ToString(); if (TxtOverlayPdfExportPath != null) TxtOverlayPdfExportPath.Text = llm.PdfExportPath ?? ""; if (ChkOverlayEnableProactiveCompact != null) ChkOverlayEnableProactiveCompact.IsChecked = llm.EnableProactiveContextCompact; if (ChkOverlayEnableSkillSystem != null) ChkOverlayEnableSkillSystem.IsChecked = llm.EnableSkillSystem; if (ChkOverlayEnableToolHooks != null) ChkOverlayEnableToolHooks.IsChecked = llm.EnableToolHooks; if (ChkOverlayEnableHookInputMutation != null) ChkOverlayEnableHookInputMutation.IsChecked = llm.EnableHookInputMutation; if (ChkOverlayEnableHookPermissionUpdate != null) ChkOverlayEnableHookPermissionUpdate.IsChecked = llm.EnableHookPermissionUpdate; if (ChkOverlayEnableCoworkVerification != null) ChkOverlayEnableCoworkVerification.IsChecked = llm.EnableCoworkVerification; SelectComboBoxByTag(CmbOverlayCoworkOnComplete, llm.CoworkOnComplete); SelectComboBoxByTag(CmbOverlayAutoPreview, llm.AutoPreview); if (ChkOverlayEnableCodeVerification != null) ChkOverlayEnableCodeVerification.IsChecked = llm.Code.EnableCodeVerification; if (ChkOverlayEnableCodeReview != null) ChkOverlayEnableCodeReview.IsChecked = llm.Code.EnableCodeReview; if (ChkOverlayEnableImageInput != null) ChkOverlayEnableImageInput.IsChecked = llm.EnableImageInput; if (ChkOverlayEnableParallelTools != null) ChkOverlayEnableParallelTools.IsChecked = llm.EnableParallelTools; if (ChkOverlayEnableProjectRules != null) ChkOverlayEnableProjectRules.IsChecked = llm.EnableProjectRules; if (ChkOverlayEnableAgentMemory != null) ChkOverlayEnableAgentMemory.IsChecked = llm.EnableAgentMemory; if (ChkOverlayEnableIbmDiagnosticLog != null) ChkOverlayEnableIbmDiagnosticLog.IsChecked = llm.EnableIbmDiagnosticLog; if (ChkOverlayEnableWorktreeTools != null) ChkOverlayEnableWorktreeTools.IsChecked = llm.Code.EnableWorktreeTools; if (ChkOverlayEnableTeamTools != null) ChkOverlayEnableTeamTools.IsChecked = llm.Code.EnableTeamTools; if (ChkOverlayEnableCronTools != null) ChkOverlayEnableCronTools.IsChecked = llm.Code.EnableCronTools; if (CmbOverlayMascotLevel != null) SelectComboBoxByTag(CmbOverlayMascotLevel, llm.Code.MascotLevel ?? "none"); if (ChkOverlayWorkflowVisualizer != null) ChkOverlayWorkflowVisualizer.IsChecked = llm.WorkflowVisualizer; if (ChkOverlayShowTotalCallStats != null) ChkOverlayShowTotalCallStats.IsChecked = llm.ShowTotalCallStats; if (ChkOverlayEnableAuditLog != null) ChkOverlayEnableAuditLog.IsChecked = llm.EnableAuditLog; if (ChkOverlayEnableDetailedLog != null) ChkOverlayEnableDetailedLog.IsChecked = llm.EnableDetailedLog; if (ChkOverlayEnableRawLlmLog != null) ChkOverlayEnableRawLlmLog.IsChecked = llm.EnableRawLlmLog; if (ChkOverlayEnableChatRainbowGlow != null) ChkOverlayEnableChatRainbowGlow.IsChecked = llm.EnableChatRainbowGlow; if (ChkOverlayEnableChatIconRandomAnim != null) ChkOverlayEnableChatIconRandomAnim.IsChecked = _settings.Settings.Launcher.EnableChatIconRandomAnimation; if (CmbOverlayChatIconGlow != null) SelectComboBoxByTag(CmbOverlayChatIconGlow, _settings.Settings.Launcher.ChatIconGlowIntensity ?? "medium"); // V2 뷰어/렌더링 전환 완료 — 토글 제거됨 } RefreshOverlayThemeCards(); RefreshOverlayServiceCards(); RefreshOverlayModeButtons(); RefreshOverlayTokenPresetCards(); RefreshOverlayServiceFieldLabels(service); RefreshOverlayServiceFieldVisibility(service); BuildOverlayRegisteredModelsPanel(service); RefreshOverlayAdvancedChoiceButtons(); RefreshOverlayRetentionButtons(); RefreshOverlayStorageSummary(); } finally { _isOverlaySettingsSyncing = false; } } private static string NormalizeOverlayService(string? service) => string.Equals(service, "sigmoid", StringComparison.OrdinalIgnoreCase) ? "claude" : (service ?? "ollama").Trim().ToLowerInvariant(); private void CommitOverlayEndpointInput(bool normalizeOnInvalid) { var service = NormalizeOverlayService(_settings.Settings.Llm.Service); var endpoint = TxtOverlayServiceEndpoint?.Text.Trim() ?? ""; ClearOverlayValidation(TxtOverlayServiceEndpoint); switch (service) { case "ollama": _settings.Settings.Llm.OllamaEndpoint = endpoint; _settings.Settings.Llm.Endpoint = endpoint; break; case "vllm": _settings.Settings.Llm.VllmEndpoint = endpoint; _settings.Settings.Llm.Endpoint = endpoint; break; default: _settings.Settings.Llm.Endpoint = endpoint; break; } if (normalizeOnInvalid && TxtOverlayServiceEndpoint != null) TxtOverlayServiceEndpoint.Text = endpoint; } private void CommitOverlayApiKeyInput() { var service = NormalizeOverlayService(_settings.Settings.Llm.Service); var apiKey = TxtOverlayServiceApiKey?.Text ?? ""; switch (service) { case "ollama": _settings.Settings.Llm.OllamaApiKey = apiKey; _settings.Settings.Llm.ApiKey = apiKey; break; case "vllm": _settings.Settings.Llm.VllmApiKey = apiKey; _settings.Settings.Llm.ApiKey = apiKey; break; case "gemini": _settings.Settings.Llm.GeminiApiKey = apiKey; _settings.Settings.Llm.ApiKey = apiKey; break; default: _settings.Settings.Llm.ClaudeApiKey = apiKey; _settings.Settings.Llm.ApiKey = apiKey; break; } } private void CommitOverlayModelInput(bool normalizeOnInvalid) { if (TxtOverlayModelInput == null || TxtOverlayModelInput.Visibility != Visibility.Visible) return; var value = TxtOverlayModelInput?.Text.Trim() ?? ""; if (string.IsNullOrWhiteSpace(value)) { MarkOverlayValidation(TxtOverlayModelInput, "모델명을 입력하세요."); if (normalizeOnInvalid && TxtOverlayModelInput != null) { TxtOverlayModelInput.Text = _settings.Settings.Llm.Model ?? ""; ClearOverlayValidation(TxtOverlayModelInput); } return; } ClearOverlayValidation(TxtOverlayModelInput); _settings.Settings.Llm.Model = value; var service = NormalizeOverlayService(_settings.Settings.Llm.Service); switch (service) { case "ollama": _settings.Settings.Llm.OllamaModel = value; break; case "vllm": _settings.Settings.Llm.VllmModel = value; break; case "gemini": _settings.Settings.Llm.GeminiModel = value; break; default: _settings.Settings.Llm.ClaudeModel = value; break; } } private bool CommitOverlayNumericInput(TextBox? textBox, int currentValue, int min, int max, Action applyValue, bool normalizeOnInvalid) { if (textBox == null) return false; if (!int.TryParse(textBox.Text?.Trim(), out var parsed)) { MarkOverlayValidation(textBox, $"{min}~{max} 사이 숫자를 입력하세요."); if (normalizeOnInvalid) { textBox.Text = Math.Clamp(currentValue, min, max).ToString(); ClearOverlayValidation(textBox); } return false; } parsed = Math.Clamp(parsed, min, max); applyValue(parsed); textBox.Text = parsed.ToString(); ClearOverlayValidation(textBox); return true; } private LlmSettings? TryGetOverlayLlmSettings() => _settings?.Settings?.Llm; private void MarkOverlayValidation(Control? control, string message) { if (control == null) return; control.BorderBrush = BrushFromHex("#DC2626"); control.ToolTip = message; } private void ClearOverlayValidation(Control? control) { if (control == null) return; control.BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; control.ClearValue(ToolTipProperty); } private void CommitOverlayModelSelection(string modelId) { _settings.Settings.Llm.Model = modelId; var service = NormalizeOverlayService(_settings.Settings.Llm.Service); switch (service) { case "ollama": _settings.Settings.Llm.OllamaModel = modelId; break; case "vllm": _settings.Settings.Llm.VllmModel = modelId; break; case "gemini": _settings.Settings.Llm.GeminiModel = modelId; break; default: _settings.Settings.Llm.ClaudeModel = modelId; break; } if (TxtOverlayModelInput != null && TxtOverlayModelInput.Visibility == Visibility.Visible) { TxtOverlayModelInput.Text = modelId; ClearOverlayValidation(TxtOverlayModelInput); } } private void ChkOverlayAiEnabled_Changed(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; _settings.Settings.AiEnabled = true; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void ChkOverlayVllmAllowInsecureTls_Changed(object sender, RoutedEventArgs e) { // vLLM SSL 우회는 모델 등록 단계에서만 관리합니다. } private void TxtOverlayServiceEndpoint_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; CommitOverlayEndpointInput(normalizeOnInvalid: false); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void TxtOverlayServiceApiKey_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; CommitOverlayApiKeyInput(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void TxtOverlayModelInput_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; CommitOverlayModelInput(normalizeOnInvalid: false); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void RefreshOverlayAdvancedChoiceButtons() { // ToggleSwitch 기반으로 바뀌면서 별도 버튼 시각 동기화는 사용하지 않습니다. } private void TxtOverlayContextCompactTriggerPercent_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; if (CommitOverlayNumericInput(TxtOverlayContextCompactTriggerPercent, _settings.Settings.Llm.ContextCompactTriggerPercent, 10, 95, value => _settings.Settings.Llm.ContextCompactTriggerPercent = value, normalizeOnInvalid: false)) PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void TxtOverlayMaxContextTokens_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; if (CommitOverlayNumericInput(TxtOverlayMaxContextTokens, _settings.Settings.Llm.MaxContextTokens, 1024, 1_000_000, value => _settings.Settings.Llm.MaxContextTokens = value, normalizeOnInvalid: false)) PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private bool CommitOverlayTemperatureInput(bool normalizeOnInvalid) { if (TxtOverlayTemperature == null) return false; if (_settings.Settings.Llm.UseAutomaticProfileTemperature) return false; var raw = TxtOverlayTemperature.Text.Trim(); if (!double.TryParse(raw, out var parsed)) { ClearOverlayValidation(TxtOverlayTemperature); if (normalizeOnInvalid) TxtOverlayTemperature.Text = Math.Round(Math.Clamp(_settings.Settings.Llm.Temperature, 0.0, 2.0), 1).ToString("0.0"); return false; } var normalized = Math.Round(Math.Clamp(parsed, 0.0, 2.0), 1); var changed = Math.Abs(_settings.Settings.Llm.Temperature - normalized) > 0.0001; _settings.Settings.Llm.Temperature = normalized; ClearOverlayValidation(TxtOverlayTemperature); if (normalizeOnInvalid || changed) TxtOverlayTemperature.Text = normalized.ToString("0.0"); return changed; } private void TxtOverlayTemperature_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; if (CommitOverlayTemperatureInput(normalizeOnInvalid: false)) PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void SldOverlayTemperature_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { var llm = TryGetOverlayLlmSettings(); if (_isOverlaySettingsSyncing || llm == null) return; if (llm.UseAutomaticProfileTemperature) return; var value = Math.Round(Math.Clamp(e.NewValue, 0.0, 2.0), 1); llm.Temperature = value; if (TxtOverlayTemperature != null) TxtOverlayTemperature.Text = value.ToString("0.0"); if (TxtOverlayTemperatureValue != null) TxtOverlayTemperatureValue.Text = value.ToString("0.0"); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void OverlayTemperatureAutoCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _settings.Settings.Llm.UseAutomaticProfileTemperature = true; RefreshOverlayTemperatureModeButtons(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void OverlayTemperatureCustomCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _settings.Settings.Llm.UseAutomaticProfileTemperature = false; RefreshOverlayTemperatureModeButtons(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void SldOverlayMaxRetryOnError_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { var llm = TryGetOverlayLlmSettings(); if (_isOverlaySettingsSyncing || llm == null) return; var value = (int)Math.Round(Math.Clamp(e.NewValue, 0, 10)); llm.MaxRetryOnError = value; if (TxtOverlayMaxRetryOnError != null) TxtOverlayMaxRetryOnError.Text = value.ToString(); if (TxtOverlayMaxRetryOnErrorValue != null) TxtOverlayMaxRetryOnErrorValue.Text = value.ToString(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void SldOverlayMaxAgentIterations_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { var llm = TryGetOverlayLlmSettings(); if (_isOverlaySettingsSyncing || llm == null) return; var value = (int)Math.Round(Math.Clamp(e.NewValue, 1, 100)); llm.MaxAgentIterations = value; if (TxtOverlayMaxAgentIterations != null) TxtOverlayMaxAgentIterations.Text = value.ToString(); if (TxtOverlayMaxAgentIterationsValue != null) TxtOverlayMaxAgentIterationsValue.Text = value.ToString(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void SldOverlayFreeTierDelaySeconds_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { var llm = TryGetOverlayLlmSettings(); if (_isOverlaySettingsSyncing || llm == null) return; var value = (int)Math.Round(Math.Clamp(e.NewValue, 0, 60)); llm.FreeTierDelaySeconds = value; if (TxtOverlayFreeTierDelaySeconds != null) TxtOverlayFreeTierDelaySeconds.Text = value.ToString(); if (TxtOverlayFreeTierDelaySecondsValue != null) TxtOverlayFreeTierDelaySecondsValue.Text = value.ToString(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void SldOverlayMaxSubAgents_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { var llm = TryGetOverlayLlmSettings(); if (_isOverlaySettingsSyncing || llm == null) return; var value = (int)Math.Round(Math.Clamp(e.NewValue, 1, 10)); llm.MaxSubAgents = value; if (TxtOverlayMaxSubAgents != null) TxtOverlayMaxSubAgents.Text = value.ToString(); if (TxtOverlayMaxSubAgentsValue != null) TxtOverlayMaxSubAgentsValue.Text = value.ToString(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void SldOverlayToolHookTimeoutMs_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { var llm = TryGetOverlayLlmSettings(); if (_isOverlaySettingsSyncing || llm == null) return; var value = (int)Math.Round(Math.Clamp(e.NewValue, 3000, 30000)); llm.ToolHookTimeoutMs = value; if (TxtOverlayToolHookTimeoutMs != null) TxtOverlayToolHookTimeoutMs.Text = value.ToString(); if (TxtOverlayToolHookTimeoutMsValue != null) TxtOverlayToolHookTimeoutMsValue.Text = $"{value / 1000}s"; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void SldOverlaySlashPopupPageSize_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { var llm = TryGetOverlayLlmSettings(); if (_isOverlaySettingsSyncing || llm == null) return; var value = (int)Math.Round(Math.Clamp(e.NewValue, 3, 20)); llm.SlashPopupPageSize = value; if (TxtOverlaySlashPopupPageSize != null) TxtOverlaySlashPopupPageSize.Text = value.ToString(); if (TxtOverlaySlashPopupPageSizeValue != null) TxtOverlaySlashPopupPageSizeValue.Text = value.ToString(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void SldOverlayMaxFavoriteSlashCommands_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { var llm = TryGetOverlayLlmSettings(); if (_isOverlaySettingsSyncing || llm == null) return; var value = (int)Math.Round(Math.Clamp(e.NewValue, 1, 30)); llm.MaxFavoriteSlashCommands = value; if (TxtOverlayMaxFavoriteSlashCommands != null) TxtOverlayMaxFavoriteSlashCommands.Text = value.ToString(); if (TxtOverlayMaxFavoriteSlashCommandsValue != null) TxtOverlayMaxFavoriteSlashCommandsValue.Text = value.ToString(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void SldOverlayMaxRecentSlashCommands_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { var llm = TryGetOverlayLlmSettings(); if (_isOverlaySettingsSyncing || llm == null) return; var value = (int)Math.Round(Math.Clamp(e.NewValue, 5, 50)); llm.MaxRecentSlashCommands = value; if (TxtOverlayMaxRecentSlashCommands != null) TxtOverlayMaxRecentSlashCommands.Text = value.ToString(); if (TxtOverlayMaxRecentSlashCommandsValue != null) TxtOverlayMaxRecentSlashCommandsValue.Text = value.ToString(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void OverlayCompactPresetCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (sender is not Border border || border.Tag is not string tag || !int.TryParse(tag, out var value)) return; _settings.Settings.Llm.ContextCompactTriggerPercent = Math.Clamp(value, 10, 95); if (TxtOverlayContextCompactTriggerPercent != null) TxtOverlayContextCompactTriggerPercent.Text = _settings.Settings.Llm.ContextCompactTriggerPercent.ToString(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void OverlayContextPresetCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (sender is not Border border || border.Tag is not string tag || !int.TryParse(tag, out var value)) return; _settings.Settings.Llm.MaxContextTokens = Math.Clamp(value, 1024, 1_000_000); if (TxtOverlayMaxContextTokens != null) TxtOverlayMaxContextTokens.Text = _settings.Settings.Llm.MaxContextTokens.ToString(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void TxtOverlayMaxRetryOnError_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; if (CommitOverlayNumericInput(TxtOverlayMaxRetryOnError, _settings.Settings.Llm.MaxRetryOnError, 0, 10, value => _settings.Settings.Llm.MaxRetryOnError = value, normalizeOnInvalid: false)) PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void TxtOverlayMaxAgentIterations_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; if (CommitOverlayNumericInput(TxtOverlayMaxAgentIterations, _settings.Settings.Llm.MaxAgentIterations, 1, 200, value => _settings.Settings.Llm.MaxAgentIterations = value, normalizeOnInvalid: false)) PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void TxtOverlayFreeTierDelaySeconds_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; if (CommitOverlayNumericInput(TxtOverlayFreeTierDelaySeconds, _settings.Settings.Llm.FreeTierDelaySeconds, 0, 60, value => _settings.Settings.Llm.FreeTierDelaySeconds = value, normalizeOnInvalid: false)) PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void TxtOverlayMaxSubAgents_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; if (CommitOverlayNumericInput(TxtOverlayMaxSubAgents, _settings.Settings.Llm.MaxSubAgents, 1, 10, value => _settings.Settings.Llm.MaxSubAgents = value, normalizeOnInvalid: false)) PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void TxtOverlaySlashPopupPageSize_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; if (CommitOverlayNumericInput(TxtOverlaySlashPopupPageSize, _settings.Settings.Llm.SlashPopupPageSize, 3, 20, value => _settings.Settings.Llm.SlashPopupPageSize = value, normalizeOnInvalid: false)) PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void TxtOverlayToolHookTimeoutMs_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; if (CommitOverlayNumericInput(TxtOverlayToolHookTimeoutMs, _settings.Settings.Llm.ToolHookTimeoutMs, 3000, 30000, value => _settings.Settings.Llm.ToolHookTimeoutMs = value, normalizeOnInvalid: false)) PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void TxtOverlayMaxFavoriteSlashCommands_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; if (CommitOverlayNumericInput(TxtOverlayMaxFavoriteSlashCommands, _settings.Settings.Llm.MaxFavoriteSlashCommands, 1, 30, value => _settings.Settings.Llm.MaxFavoriteSlashCommands = value, normalizeOnInvalid: false)) PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void TxtOverlayMaxRecentSlashCommands_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; if (CommitOverlayNumericInput(TxtOverlayMaxRecentSlashCommands, _settings.Settings.Llm.MaxRecentSlashCommands, 5, 50, value => _settings.Settings.Llm.MaxRecentSlashCommands = value, normalizeOnInvalid: false)) PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void TxtOverlayPdfExportPath_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing || TxtOverlayPdfExportPath == null) return; _settings.Settings.Llm.PdfExportPath = TxtOverlayPdfExportPath.Text.Trim(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void TxtOverlayPlanDiffMediumCount_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; if (CommitOverlayNumericInput(TxtOverlayPlanDiffMediumCount, _settings.Settings.Llm.PlanDiffSeverityMediumCount, 1, 999, value => _settings.Settings.Llm.PlanDiffSeverityMediumCount = value, normalizeOnInvalid: false)) PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void TxtOverlayPlanDiffHighCount_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; if (CommitOverlayNumericInput(TxtOverlayPlanDiffHighCount, _settings.Settings.Llm.PlanDiffSeverityHighCount, 1, 999, value => _settings.Settings.Llm.PlanDiffSeverityHighCount = value, normalizeOnInvalid: false)) PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void TxtOverlayPlanDiffMediumRatio_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; if (CommitOverlayNumericInput(TxtOverlayPlanDiffMediumRatio, _settings.Settings.Llm.PlanDiffSeverityMediumRatioPercent, 1, 100, value => _settings.Settings.Llm.PlanDiffSeverityMediumRatioPercent = value, normalizeOnInvalid: false)) PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void TxtOverlayPlanDiffHighRatio_LostFocus(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; if (CommitOverlayNumericInput(TxtOverlayPlanDiffHighRatio, _settings.Settings.Llm.PlanDiffSeverityHighRatioPercent, 1, 100, value => _settings.Settings.Llm.PlanDiffSeverityHighRatioPercent = value, normalizeOnInvalid: false)) PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void OverlayBrowseSkillFolderBtn_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { var dlg = new System.Windows.Forms.FolderBrowserDialog { Description = "스킬 파일이 있는 폴더를 선택하세요", ShowNewFolderButton = true, }; var current = _settings.Settings.Llm.SkillsFolderPath; if (!string.IsNullOrWhiteSpace(current) && Directory.Exists(current)) dlg.SelectedPath = current; if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return; _settings.Settings.Llm.SkillsFolderPath = dlg.SelectedPath; SkillService.LoadSkills(dlg.SelectedPath, GetCurrentWorkFolder()); RefreshOverlayEtcPanels(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void OverlayOpenSkillFolderBtn_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { var folder = !string.IsNullOrWhiteSpace(_settings.Settings.Llm.SkillsFolderPath) && Directory.Exists(_settings.Settings.Llm.SkillsFolderPath) ? _settings.Settings.Llm.SkillsFolderPath : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills"); if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); try { System.Diagnostics.Process.Start("explorer.exe", folder); } catch { } } private void OverlayAddHookBtn_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => ShowOverlayHookEditDialog(null, -1); private void OverlayOpenAuditLogBtn_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { // 클릭 효과 리셋 if (sender is Border border) { border.Opacity = 1.0; border.RenderTransform = null; } try { System.Diagnostics.Process.Start("explorer.exe", Services.AuditLogService.GetAuditFolder()); } catch { } } private void BtnBrowsePdfExportPath_Click(object sender, MouseButtonEventArgs e) { // 클릭 효과 리셋 if (sender is Border border) { border.Opacity = 1.0; border.RenderTransform = null; } var dlg = new System.Windows.Forms.FolderBrowserDialog { Description = "PDF 내보내기 기본 폴더를 선택하세요", ShowNewFolderButton = true, UseDescriptionForTitle = true, }; var current = _settings.Settings.Llm.PdfExportPath; if (!string.IsNullOrWhiteSpace(current) && Directory.Exists(current)) dlg.SelectedPath = current; if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return; _settings.Settings.Llm.PdfExportPath = dlg.SelectedPath; if (TxtOverlayPdfExportPath != null) TxtOverlayPdfExportPath.Text = dlg.SelectedPath; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } /// 설정 오버레이 액션 버튼 공통 호버/클릭 효과. private void OverlayActionBtn_MouseEnter(object sender, MouseEventArgs e) { if (sender is Border border) { border.Opacity = 0.85; border.Background = TryFindResource("ItemActiveBackground") as Brush ?? TryFindResource("ItemHoverBackground") as Brush ?? border.Background; } } private void OverlayActionBtn_MouseLeave(object sender, MouseEventArgs e) { if (sender is Border border) { border.Opacity = 1.0; border.Background = TryFindResource("ItemHoverBackground") as Brush ?? border.Background; } } private void OverlayActionBtn_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (sender is Border border) { border.Opacity = 0.65; border.RenderTransform = new ScaleTransform(0.96, 0.96); border.RenderTransformOrigin = new Point(0.5, 0.5); } } private void ChkOverlayEnableDragDropAiActions_Changed(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing || ChkOverlayEnableDragDropAiActions == null) return; _settings.Settings.Llm.EnableDragDropAiActions = ChkOverlayEnableDragDropAiActions.IsChecked == true; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void ChkOverlayDragDropAutoSend_Changed(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing || ChkOverlayDragDropAutoSend == null) return; _settings.Settings.Llm.DragDropAutoSend = ChkOverlayDragDropAutoSend.IsChecked == true; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void ChkOverlayWorkflowVisualizer_Changed(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing || ChkOverlayWorkflowVisualizer == null) return; _settings.Settings.Llm.WorkflowVisualizer = ChkOverlayWorkflowVisualizer.IsChecked == true; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void ChkOverlayShowTotalCallStats_Changed(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing || ChkOverlayShowTotalCallStats == null) return; _settings.Settings.Llm.ShowTotalCallStats = ChkOverlayShowTotalCallStats.IsChecked == true; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void ChkOverlayEnableAuditLog_Changed(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing || ChkOverlayEnableAuditLog == null) return; _settings.Settings.Llm.EnableAuditLog = ChkOverlayEnableAuditLog.IsChecked == true; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void ChkOverlayEnableDetailedLog_Changed(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing || ChkOverlayEnableDetailedLog == null) return; var enabled = ChkOverlayEnableDetailedLog.IsChecked == true; _settings.Settings.Llm.EnableDetailedLog = enabled; WorkflowLogService.IsEnabled = enabled; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void ChkOverlayEnableRawLlmLog_Changed(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing || ChkOverlayEnableRawLlmLog == null) return; var enabled = ChkOverlayEnableRawLlmLog.IsChecked == true; _settings.Settings.Llm.EnableRawLlmLog = enabled; WorkflowLogService.IsRawLogEnabled = enabled; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void ChkOverlayFeatureToggle_Changed(object sender, RoutedEventArgs e) { if (_isOverlaySettingsSyncing) return; ApplyOverlaySettingsChanges(showToast: false, closeOverlay: false); } private void CmbOverlayMascotLevel_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { if (_isOverlaySettingsSyncing) return; ApplyOverlaySettingsChanges(showToast: false, closeOverlay: false); } private void CmbOverlayCoworkOnComplete_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { if (_isOverlaySettingsSyncing) return; ApplyOverlaySettingsChanges(showToast: false, closeOverlay: false); } private void CmbOverlayChatIconGlow_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { if (_isOverlaySettingsSyncing) return; ApplyOverlaySettingsChanges(showToast: false, closeOverlay: false); } private static void SelectComboBoxByTag(System.Windows.Controls.ComboBox? cmb, string? tag) { if (cmb == null) return; foreach (var item in cmb.Items) { if (item is System.Windows.Controls.ComboBoxItem ci && string.Equals(ci.Tag?.ToString(), tag, StringComparison.OrdinalIgnoreCase)) { cmb.SelectedItem = ci; return; } } if (cmb.Items.Count > 0) cmb.SelectedIndex = 0; // fallback to "none" } private void OverlayNav_Checked(object sender, RoutedEventArgs e) { if (sender is not RadioButton rb || rb.Tag is not string tag) return; SetOverlaySection(tag); } private void SetOverlaySection(string tag) { if (OverlaySectionService == null || OverlaySectionQuick == null || OverlaySectionDetail == null) return; var section = string.IsNullOrWhiteSpace(tag) ? "basic" : tag.Trim().ToLowerInvariant(); var showBasic = section == "basic"; var showChat = section == "chat"; var showShared = section == "shared"; var showCowork = section == "cowork"; var showCode = section == "code"; var showDev = section == "dev"; var showTools = section == "tools"; var showSkill = section == "skill"; var showBlock = section == "block"; OverlaySectionService.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed; OverlaySectionQuick.Visibility = showShared ? Visibility.Visible : Visibility.Collapsed; OverlaySectionDetail.Visibility = Visibility.Visible; var headingTitle = section switch { "chat" => "채팅 설정", "shared" => "코워크/코드 공통 설정", "cowork" => "코워크 설정", "code" => "코드 설정", "dev" => "개발자 설정", "tools" => "도구 설정", "skill" => "스킬 설정", "block" => "차단 설정", _ => "공통 설정" }; var headingDescription = section switch { "chat" => "Chat 탭에서 쓰는 입력/내보내기 같은 채팅 전용 설정입니다.", "shared" => "Cowork와 Code에서 함께 쓰는 문맥/압축 관련 기본 설정입니다.", "cowork" => "문서/업무 협업 흐름에 맞춘 코워크 전용 설정입니다.", "code" => "코드 작업, 검증, 개발 도구 사용에 맞춘 설정입니다.", "dev" => "실행 이력, 감사, 시각화 같은 개발자용 설정입니다.", "tools" => "AX Agent가 사용할 도구와 훅 동작을 관리합니다.", "skill" => "슬래시 스킬, 스킬 폴더, 폴백 모델, MCP 연결을 관리합니다.", "block" => "에이전트가 접근하거나 수정하면 안 되는 경로와 형식을 관리합니다.", _ => "Chat, Cowork, Code에서 공통으로 쓰는 기본 설정입니다." }; if (OverlayTopHeadingTitle != null) OverlayTopHeadingTitle.Text = headingTitle; if (OverlayTopHeadingDescription != null) OverlayTopHeadingDescription.Text = headingDescription; if (OverlayAnchorCommon != null) OverlayAnchorCommon.Text = headingTitle; if (OverlayAiEnabledRow != null) OverlayAiEnabledRow.Visibility = Visibility.Collapsed; if (OverlayThemePanel != null) OverlayThemePanel.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed; if (OverlayThemeStylePanel != null) OverlayThemeStylePanel.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed; if (OverlayPdfExportPathRow != null) OverlayPdfExportPathRow.Visibility = showChat ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleImageInput != null) OverlayToggleImageInput.Visibility = showChat ? Visibility.Visible : Visibility.Collapsed; if (OverlayModelEditorPanel != null) OverlayModelEditorPanel.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed; if (OverlayAnchorPermission != null) OverlayAnchorPermission.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed; if (OverlayTlsRow != null) OverlayTlsRow.Visibility = showChat ? Visibility.Visible : Visibility.Collapsed; if (OverlayAnchorAdvanced != null) OverlayAnchorAdvanced.Visibility = showShared ? Visibility.Visible : Visibility.Collapsed; if (TxtOverlayContextCompactTriggerPercent != null) TxtOverlayContextCompactTriggerPercent.Visibility = Visibility.Collapsed; if (OverlayMaxContextTokensRow != null) OverlayMaxContextTokensRow.Visibility = showShared ? Visibility.Visible : Visibility.Collapsed; if (OverlayTemperatureRow != null) OverlayTemperatureRow.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed; if (OverlayMaxRetryRow != null) OverlayMaxRetryRow.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed; if (OverlayMaxAgentIterationsRow != null) OverlayMaxAgentIterationsRow.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed; if (OverlayDeveloperRuntimePanel != null) OverlayDeveloperRuntimePanel.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed; if (OverlayDeveloperExtraPanel != null) OverlayDeveloperExtraPanel.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed; if (OverlayAdvancedTogglePanel != null) OverlayAdvancedTogglePanel.Visibility = showDev || showCowork || showCode || showTools || showSkill ? Visibility.Visible : Visibility.Collapsed; if (OverlayToolsInfoPanel != null) OverlayToolsInfoPanel.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed; if (OverlayToolsRuntimePanel != null) OverlayToolsRuntimePanel.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed; if (OverlayToolRegistrySection != null) OverlayToolRegistrySection.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed; if (OverlaySkillInfoPanel != null) OverlaySkillInfoPanel.Visibility = showSkill ? Visibility.Visible : Visibility.Collapsed; if (OverlaySkillRuntimePanel != null) OverlaySkillRuntimePanel.Visibility = showSkill ? Visibility.Visible : Visibility.Collapsed; if (OverlayBlockInfoPanel != null) OverlayBlockInfoPanel.Visibility = showBlock ? Visibility.Visible : Visibility.Collapsed; if (OverlayBlockRuntimePanel != null) OverlayBlockRuntimePanel.Visibility = showBlock ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleProactiveCompact != null) OverlayToggleProactiveCompact.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleSkillSystem != null) OverlayToggleSkillSystem.Visibility = showSkill ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleToolHooks != null) OverlayToggleToolHooks.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleHookInputMutation != null) OverlayToggleHookInputMutation.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleHookPermissionUpdate != null) OverlayToggleHookPermissionUpdate.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleCoworkVerification != null) OverlayToggleCoworkVerification.Visibility = showCowork ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleCoworkOnComplete != null) OverlayToggleCoworkOnComplete.Visibility = showCowork ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleAutoPreview != null) OverlayToggleAutoPreview.Visibility = showCowork ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleCodeVerification != null) OverlayToggleCodeVerification.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleCodeReview != null) OverlayToggleCodeReview.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleParallelTools != null) OverlayToggleParallelTools.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleProjectRules != null) OverlayToggleProjectRules.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleAgentMemory != null) OverlayToggleAgentMemory.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleIbmDiagnostic != null) OverlayToggleIbmDiagnostic.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleWorktreeTools != null) OverlayToggleWorktreeTools.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleTeamTools != null) OverlayToggleTeamTools.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleCronTools != null) OverlayToggleCronTools.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed; if (OverlayToggleMascotCharacter != null) OverlayToggleMascotCharacter.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed; if (OverlaySectionGlowEffects != null) OverlaySectionGlowEffects.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed; if (OverlaySectionIconEffects != null) OverlaySectionIconEffects.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed; // V2 전환 완료 — OverlaySectionPlanViewer 제거됨 if (showTools || showSkill || showBlock) RefreshOverlayEtcPanels(); } private void RefreshOverlaySettingsPanel() { // 기본 컨트롤 상태만 동기적으로 설정 (빠름) RefreshOverlayVisualState(loadDeferredInputs: true); // 무거운 패널 빌드(스킬/MCP/도구 목록 등)는 UI 프레임 렌더링 후 비동기 지연 실행 // → 스트리밍 중 설정 열기 시 UI 프리즈 방지 Dispatcher.BeginInvoke(RefreshOverlayEtcPanels, System.Windows.Threading.DispatcherPriority.Background); } private void RefreshOverlayRetentionButtons() { // 대화 관리 섹션이 오버레이에서 제거됨 (설정 탭에서 관리) } private void ApplyOverlayRetentionButtonState(Button? button, bool selected) { if (button == null) return; var accent = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue; var border = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray; var hint = TryFindResource("HintBackground") as Brush ?? Brushes.Transparent; var primary = TryFindResource("PrimaryText") as Brush ?? Brushes.Black; var secondary = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; button.Background = selected ? hint : Brushes.Transparent; button.BorderBrush = selected ? accent : border; button.BorderThickness = new Thickness(1); button.Foreground = selected ? accent : primary; button.FontWeight = selected ? FontWeights.SemiBold : FontWeights.Normal; button.Cursor = Cursors.Hand; } private void RefreshOverlayStorageSummary() { if (OverlayStorageSummaryText == null || OverlayStorageDriveText == null) return; var report = StorageAnalyzer.Analyze(); var appTotal = report.Conversations + report.AuditLogs + report.Logs + report.CodeIndex + report.EmbeddingDb + report.ClipboardHistory + report.Plugins + report.Skills + report.Settings; OverlayStorageSummaryText.Text = $"앱 전체 사용량: {FormatStorageBytes(appTotal)}"; if (!string.IsNullOrWhiteSpace(report.DriveLabel) && report.DriveTotalSpace > 0) { var used = report.DriveTotalSpace - report.DriveFreeSpace; var percent = report.DriveTotalSpace == 0 ? 0 : (int)Math.Round((double)used / report.DriveTotalSpace * 100); OverlayStorageDriveText.Text = $"{report.DriveLabel} · 사용 {percent}% · 여유 {FormatStorageBytes(report.DriveFreeSpace)}"; } else { OverlayStorageDriveText.Text = "로컬 앱 데이터 폴더 기준 사용량입니다."; } } private void BtnOverlayRetention_Click(object sender, RoutedEventArgs e) { if (sender is not FrameworkElement element) return; var retainDays = element.Name switch { "BtnOverlayRetention7" => 7, "BtnOverlayRetention30" => 30, "BtnOverlayRetention90" => 90, "BtnOverlayRetentionUnlimited" => 0, _ => _settings.Settings.Llm.RetentionDays }; _settings.Settings.Llm.RetentionDays = retainDays; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); RefreshOverlayRetentionButtons(); } private void BtnOverlayStorageRefresh_Click(object sender, RoutedEventArgs e) { RefreshOverlayStorageSummary(); } private void BtnOverlayDeleteAllConversations_Click(object sender, RoutedEventArgs e) { BtnDeleteAll_Click(sender, e); RefreshOverlayStorageSummary(); } private void BtnOverlayStorageCleanup_Click(object sender, RoutedEventArgs e) { var retainDays = Math.Max(0, _settings.Settings.Llm.RetentionDays); var cleanedBytes = StorageAnalyzer.Cleanup( retainDays, cleanConversations: false, cleanAuditLogs: true, cleanLogs: true, cleanCodeIndex: true, cleanClipboard: true); RefreshOverlayStorageSummary(); CustomMessageBox.Show( cleanedBytes > 0 ? $"저장 공간을 정리했습니다.\n확보된 공간: {StorageAnalyzer.FormatSize(cleanedBytes)}" : "정리할 항목이 없었습니다.", "저장 공간 정리", MessageBoxButton.OK, MessageBoxImage.Information); } private static string FormatStorageBytes(long bytes) { if (bytes >= 1024L * 1024 * 1024) return $"{bytes / 1024.0 / 1024 / 1024:F1} GB"; if (bytes >= 1024L * 1024) return $"{bytes / 1024.0 / 1024:F1} MB"; if (bytes >= 1024L) return $"{bytes / 1024.0:F0} KB"; return $"{bytes} B"; } private void RefreshOverlayEtcPanels() { var llm = _settings.Settings.Llm; if (OverlaySkillsFolderPathText != null) { var defaultFolder = System.IO.Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills"); OverlaySkillsFolderPathText.Text = string.IsNullOrWhiteSpace(llm.SkillsFolderPath) ? defaultFolder : llm.SkillsFolderPath.Trim(); } if (TxtOverlaySlashPopupPageSize != null) TxtOverlaySlashPopupPageSize.Text = Math.Clamp(llm.SlashPopupPageSize, 3, 20).ToString(); if (SldOverlaySlashPopupPageSize != null) SldOverlaySlashPopupPageSize.Value = Math.Clamp(llm.SlashPopupPageSize, 3, 20); if (TxtOverlaySlashPopupPageSizeValue != null) TxtOverlaySlashPopupPageSizeValue.Text = Math.Clamp(llm.SlashPopupPageSize, 3, 20).ToString(); if (TxtOverlayToolHookTimeoutMs != null) TxtOverlayToolHookTimeoutMs.Text = Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000).ToString(); if (SldOverlayToolHookTimeoutMs != null) SldOverlayToolHookTimeoutMs.Value = Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000); if (TxtOverlayToolHookTimeoutMsValue != null) TxtOverlayToolHookTimeoutMsValue.Text = $"{Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000) / 1000}s"; if (TxtOverlayMaxFavoriteSlashCommands != null) TxtOverlayMaxFavoriteSlashCommands.Text = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30).ToString(); if (SldOverlayMaxFavoriteSlashCommands != null) SldOverlayMaxFavoriteSlashCommands.Value = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30); if (TxtOverlayMaxFavoriteSlashCommandsValue != null) TxtOverlayMaxFavoriteSlashCommandsValue.Text = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30).ToString(); if (TxtOverlayMaxRecentSlashCommands != null) TxtOverlayMaxRecentSlashCommands.Text = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50).ToString(); if (SldOverlayMaxRecentSlashCommands != null) SldOverlayMaxRecentSlashCommands.Value = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50); if (TxtOverlayMaxRecentSlashCommandsValue != null) TxtOverlayMaxRecentSlashCommandsValue.Text = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50).ToString(); if (ChkOverlayEnableDragDropAiActions != null) ChkOverlayEnableDragDropAiActions.IsChecked = llm.EnableDragDropAiActions; if (ChkOverlayDragDropAutoSend != null) ChkOverlayDragDropAutoSend.IsChecked = llm.DragDropAutoSend; BuildOverlayBlockedItems(); BuildOverlayHookCards(); BuildOverlaySkillListPanel(); BuildOverlayFallbackModelsPanel(); BuildOverlayMcpServerCards(); BuildOverlayToolRegistryPanel(); } private void BuildOverlayBlockedItems() { if (OverlayBlockedPathsPanel != null) { OverlayBlockedPathsPanel.Children.Clear(); foreach (var path in _settings.Settings.Llm.BlockedPaths.Where(x => !string.IsNullOrWhiteSpace(x))) { OverlayBlockedPathsPanel.Children.Add(new Border { Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.White, BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(6), Padding = new Thickness(8, 4, 8, 4), Margin = new Thickness(0, 0, 0, 4), Child = new TextBlock { Text = path, FontSize = 11.5, FontFamily = new FontFamily("Consolas, Malgun Gothic"), Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray } }); } } if (OverlayBlockedExtensionsPanel != null) { OverlayBlockedExtensionsPanel.Children.Clear(); foreach (var ext in _settings.Settings.Llm.BlockedExtensions.Where(x => !string.IsNullOrWhiteSpace(x))) { OverlayBlockedExtensionsPanel.Children.Add(new Border { Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.White, BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(6), Padding = new Thickness(8, 4, 8, 4), Margin = new Thickness(0, 0, 6, 6), Child = new TextBlock { Text = ext, FontSize = 11.5, FontFamily = new FontFamily("Consolas, Malgun Gothic"), Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray } }); } } } private void BuildOverlaySkillListPanel() { if (OverlaySkillListPanel == null) return; OverlaySkillListPanel.Children.Clear(); var skills = SkillService.Skills.ToList(); if (skills.Count == 0) { OverlaySkillListPanel.Children.Add(new TextBlock { Text = "로드된 스킬이 없습니다.", FontSize = 11, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray }); return; } var unavailable = skills .Where(skill => !skill.IsAvailable) .OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase) .ToList(); var autoSkills = SkillService.GetAutoSkills(_activeTab).ToList(); var directSkills = skills .Where(skill => skill.IsAvailable && skill.UserInvocable && !autoSkills.Any(autoSkill => string.Equals(autoSkill.Name, skill.Name, StringComparison.OrdinalIgnoreCase))) .OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase) .ToList(); AddOverlaySkillSection("overlay-skill-direct", "직접 호출 스킬", "슬래시(/)로 직접 실행하는 스킬입니다.", directSkills, "#2563EB"); AddOverlaySkillSection("overlay-skill-auto", "자동/조건부 스킬", "조건에 따라 자동으로 붙거나 보조적으로 동작하는 스킬입니다.", autoSkills, "#0F766E"); AddOverlaySkillSection("overlay-skill-unavailable", "현재 사용 불가", "필요한 런타임이 없어 지금은 호출되지 않는 스킬입니다.", unavailable, "#9A3412"); } private void AddOverlaySkillSection(string key, string title, string subtitle, List skills, string accentHex) { if (OverlaySkillListPanel == null || skills.Count == 0) return; var body = new StackPanel(); foreach (var skill in skills) { var card = new Border { Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.White, BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 8, 10, 8), Margin = new Thickness(0, 0, 0, 6), }; var stack = new StackPanel(); stack.Children.Add(new TextBlock { Text = "/" + skill.Name, FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black }); if (!string.IsNullOrWhiteSpace(skill.Label) && !string.Equals(skill.Label.Trim(), skill.Name, StringComparison.OrdinalIgnoreCase)) { stack.Children.Add(new TextBlock { Text = skill.Label.Trim(), Margin = new Thickness(0, 3, 0, 0), FontSize = 11, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray }); } stack.Children.Add(new TextBlock { Text = string.IsNullOrWhiteSpace(skill.Description) ? (string.IsNullOrWhiteSpace(skill.Label) ? skill.Name : skill.Label) : skill.Description, Margin = new Thickness(0, 4, 0, 0), FontSize = 11, TextWrapping = TextWrapping.Wrap, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray }); card.Child = stack; body.Children.Add(card); } OverlaySkillListPanel.Children.Add(CreateOverlayCollapsibleSection( key, $"{title} ({skills.Count})", subtitle, body, defaultExpanded: false, accentHex: accentHex)); } private void BuildOverlayFallbackModelsPanel() { if (OverlayFallbackModelsPanel == null) return; OverlayFallbackModelsPanel.Children.Clear(); var llm = _settings.Settings.Llm; var sections = new[] { ("ollama", "Ollama"), ("vllm", "vLLM"), ("gemini", "Gemini"), ("claude", "Claude") }; foreach (var (service, label) in sections) { var candidates = GetModelCandidates(service); OverlayFallbackModelsPanel.Children.Add(new TextBlock { Text = label, FontSize = 11.5, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, Margin = new Thickness(0, 0, 0, 6) }); if (candidates.Count == 0) { OverlayFallbackModelsPanel.Children.Add(new TextBlock { Text = "등록된 모델 없음", FontSize = 10.5, Margin = new Thickness(8, 0, 0, 8), Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray }); continue; } foreach (var candidate in candidates) { var enabled = llm.FallbackModels.Any(x => x.Equals(candidate.Id, StringComparison.OrdinalIgnoreCase)); var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition()); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var nameText = new TextBlock { Text = candidate.Label, FontSize = 11.5, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black }; var stateText = new TextBlock { Text = enabled ? "사용" : "미사용", FontSize = 10.5, Foreground = enabled ? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue) : (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray), Margin = new Thickness(12, 0, 0, 0) }; Grid.SetColumn(stateText, 1); grid.Children.Add(nameText); grid.Children.Add(stateText); OverlayFallbackModelsPanel.Children.Add(new Border { Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.White, BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 7, 10, 7), Margin = new Thickness(0, 0, 0, 6), Child = grid }); } } } private void BuildOverlayMcpServerCards() { if (OverlayMcpServerListPanel == null) return; OverlayMcpServerListPanel.Children.Clear(); var servers = _settings.Settings.Llm.McpServers; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue; var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray; var itemBackground = TryFindResource("ItemBackground") as Brush ?? Brushes.White; var cardBackground = TryFindResource("LauncherBackground") as Brush ?? Brushes.White; if (servers == null || servers.Count == 0) { OverlayMcpServerListPanel.Children.Add(new TextBlock { Text = "등록된 MCP 서버가 없습니다.", FontSize = 11, Foreground = secondaryText }); return; } Border CreateActionChip(string text, Brush foreground, Action onClick) { var border = new Border { Background = Brushes.Transparent, BorderBrush = Brushes.Transparent, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(8), Padding = new Thickness(8, 4, 8, 4), Margin = new Thickness(6, 0, 0, 0), Cursor = Cursors.Hand, }; var label = new TextBlock { Text = text, FontSize = 11, FontWeight = FontWeights.SemiBold, Foreground = foreground, VerticalAlignment = VerticalAlignment.Center, }; border.Child = label; border.MouseEnter += (_, _) => { border.Background = TryFindResource("HintBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(20, 0, 0, 0)); border.BorderBrush = borderBrush; }; border.MouseLeave += (_, _) => { border.Background = Brushes.Transparent; border.BorderBrush = Brushes.Transparent; }; border.MouseLeftButtonUp += (_, _) => onClick(); return border; } for (int index = 0; index < servers.Count; index++) { var server = servers[index]; var card = new Border { Background = cardBackground, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(8), Padding = new Thickness(12, 10, 12, 10), Margin = new Thickness(0, 0, 0, 6) }; var root = new StackPanel(); var header = new Grid(); header.ColumnDefinitions.Add(new ColumnDefinition()); header.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var title = new TextBlock { Text = server.Name, FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center }; header.Children.Add(title); var actions = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, }; actions.Children.Add(CreateActionChip(server.Enabled ? "비활성화" : "활성화", accentBrush, () => { _settings.Settings.Llm.McpServers[index].Enabled = !_settings.Settings.Llm.McpServers[index].Enabled; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); BuildOverlayMcpServerCards(); })); actions.Children.Add(CreateActionChip("삭제", BrushFromHex("#DC2626"), () => { var result = CustomMessageBox.Show($"'{server.Name}' 서버를 삭제하시겠습니까?", "MCP 서버 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result != MessageBoxResult.Yes) return; _settings.Settings.Llm.McpServers.RemoveAt(index); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); BuildOverlayMcpServerCards(); })); Grid.SetColumn(actions, 1); header.Children.Add(actions); root.Children.Add(header); var commandCard = new Border { Background = itemBackground, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 8, 10, 8), Margin = new Thickness(0, 8, 0, 0), Child = new StackPanel { Children = { new TextBlock { Text = server.Command, FontSize = 10.8, TextWrapping = TextWrapping.Wrap, Foreground = primaryText, }, new TextBlock { Text = server.Enabled ? "활성 상태" : "비활성 상태", Margin = new Thickness(0, 4, 0, 0), FontSize = 10.5, Foreground = secondaryText, } } } }; root.Children.Add(commandCard); card.Child = root; OverlayMcpServerListPanel.Children.Add(card); } } private void BtnOverlayAddMcpServer_Click(object sender, RoutedEventArgs e) { var nameDialog = new InputDialog("MCP 서버 추가", "서버 이름:", placeholder: "예: my-mcp-server") { Owner = this }; if (nameDialog.ShowDialog() != true || string.IsNullOrWhiteSpace(nameDialog.ResponseText)) return; var commandDialog = new InputDialog("MCP 서버 추가", "실행 명령:", placeholder: "예: npx -y @modelcontextprotocol/server-filesystem") { Owner = this }; if (commandDialog.ShowDialog() != true || string.IsNullOrWhiteSpace(commandDialog.ResponseText)) return; _settings.Settings.Llm.McpServers.Add(new Models.McpServerEntry { Name = nameDialog.ResponseText.Trim(), Command = commandDialog.ResponseText.Trim(), Enabled = true, }); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); BuildOverlayMcpServerCards(); } private void BuildOverlayToolRegistryPanel() { if (OverlayToolRegistryPanel == null) return; OverlayToolRegistryPanel.Children.Clear(); var grouped = _toolRegistry.All .GroupBy(tool => GetOverlayToolCategory(tool.Name)) .OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase) .ToList(); foreach (var group in grouped) { var body = new StackPanel(); foreach (var tool in group.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase)) { var isDisabled = _settings.Settings.Llm.DisabledTools.Contains(tool.Name, StringComparer.OrdinalIgnoreCase); var row = new Border { Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.White, BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 8, 10, 8), Margin = new Thickness(0, 0, 0, 6), }; var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition()); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var rowStack = new StackPanel { Margin = new Thickness(0, 0, 12, 0) }; rowStack.Children.Add(new TextBlock { Text = tool.Name, FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = isDisabled ? (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray) : (TryFindResource("PrimaryText") as Brush ?? Brushes.Black) }); rowStack.Children.Add(new TextBlock { Text = (isDisabled ? "비활성" : "활성") + " · " + tool.Description, Margin = new Thickness(0, 4, 0, 0), FontSize = 10.8, TextWrapping = TextWrapping.Wrap, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray }); grid.Children.Add(rowStack); var toggle = new CheckBox { IsChecked = !isDisabled, VerticalAlignment = VerticalAlignment.Center, Style = TryFindResource("ToggleSwitch") as Style, }; toggle.Checked += (_, _) => { _settings.Settings.Llm.DisabledTools.RemoveAll(name => string.Equals(name, tool.Name, StringComparison.OrdinalIgnoreCase)); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); RefreshOverlayEtcPanels(); }; toggle.Unchecked += (_, _) => { if (!_settings.Settings.Llm.DisabledTools.Contains(tool.Name, StringComparer.OrdinalIgnoreCase)) _settings.Settings.Llm.DisabledTools.Add(tool.Name); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); RefreshOverlayEtcPanels(); }; Grid.SetColumn(toggle, 1); grid.Children.Add(toggle); row.Child = grid; body.Children.Add(row); } OverlayToolRegistryPanel.Children.Add(CreateOverlayCollapsibleSection( "overlay-tool-" + group.Key, $"{group.Key} ({group.Count()})", "카테고리별 도구 목록입니다. 펼치면 상세 이름과 사용 여부를 바로 바꿀 수 있습니다.", body, defaultExpanded: false, accentHex: "#4F46E5")); } } private void BuildOverlayHookCards() { if (OverlayHookListPanel == null) return; OverlayHookListPanel.Children.Clear(); var hooks = _settings.Settings.Llm.AgentHooks; if (hooks.Count == 0) { OverlayHookListPanel.Children.Add(new TextBlock { Text = "등록된 훅이 없습니다.", FontSize = 11, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray }); return; } var body = new StackPanel(); var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue; 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.WhiteSmoke, BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 8, 10, 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()); 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, }; toggle.Checked += (_, _) => { hook.Enabled = true; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); }; toggle.Unchecked += (_, _) => { hook.Enabled = false; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); }; Grid.SetColumn(toggle, 0); grid.Children.Add(toggle); var info = new StackPanel(); var header = new StackPanel { Orientation = Orientation.Horizontal }; header.Children.Add(new TextBlock { Text = hook.Name, FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = primaryText, }); header.Children.Add(new Border { Background = BrushFromHex(hook.Timing == "pre" ? "#FFEDD5" : "#DCFCE7"), BorderBrush = BrushFromHex(hook.Timing == "pre" ? "#FDBA74" : "#86EFAC"), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(5), Padding = new Thickness(5, 1, 5, 1), Margin = new Thickness(6, 0, 0, 0), Child = new TextBlock { Text = hook.Timing == "pre" ? "PRE" : "POST", FontSize = 9.5, FontWeight = FontWeights.Bold, Foreground = BrushFromHex(hook.Timing == "pre" ? "#9A3412" : "#166534"), } }); if (!string.IsNullOrWhiteSpace(hook.ToolName) && hook.ToolName != "*") { header.Children.Add(new Border { Background = BrushFromHex("#EEF2FF"), BorderBrush = BrushFromHex("#C7D2FE"), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(5), Padding = new Thickness(5, 1, 5, 1), Margin = new Thickness(6, 0, 0, 0), Child = new TextBlock { Text = hook.ToolName, FontSize = 9.5, Foreground = BrushFromHex("#3730A3"), } }); } info.Children.Add(header); info.Children.Add(new TextBlock { Text = Path.GetFileName(hook.ScriptPath), Margin = new Thickness(0, 4, 0, 0), FontSize = 11, Foreground = secondaryText }); if (!string.IsNullOrWhiteSpace(hook.Arguments)) { info.Children.Add(new TextBlock { Text = hook.Arguments, Margin = new Thickness(0, 3, 0, 0), FontSize = 10.5, TextWrapping = TextWrapping.Wrap, Foreground = secondaryText }); } Grid.SetColumn(info, 1); grid.Children.Add(info); var editBtn = new Border { Cursor = Cursors.Hand, Padding = new Thickness(6), Margin = new Thickness(6, 0, 2, 0), VerticalAlignment = VerticalAlignment.Center, Child = new TextBlock { Text = "\uE70F", FontFamily = s_segoeIconFont, FontSize = 12, Foreground = accentBrush, } }; editBtn.MouseLeftButtonUp += (_, _) => ShowOverlayHookEditDialog(hooks[idx], idx); Grid.SetColumn(editBtn, 2); grid.Children.Add(editBtn); var deleteBtn = new Border { Cursor = Cursors.Hand, Padding = new Thickness(6), VerticalAlignment = VerticalAlignment.Center, Child = new TextBlock { Text = "\uE74D", FontFamily = s_segoeIconFont, FontSize = 12, Foreground = BrushFromHex("#DC2626"), } }; deleteBtn.MouseLeftButtonUp += (_, _) => { hooks.RemoveAt(idx); BuildOverlayHookCards(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); }; Grid.SetColumn(deleteBtn, 3); grid.Children.Add(deleteBtn); card.Child = grid; body.Children.Add(card); } OverlayHookListPanel.Children.Add(CreateOverlayCollapsibleSection( "overlay-hooks", $"등록된 훅 ({hooks.Count})", "도구 실행 전후에 연결되는 스크립트입니다.", body, defaultExpanded: false, accentHex: "#0F766E")); } private Border CreateOverlayCollapsibleSection(string key, string title, string subtitle, UIElement content, bool defaultExpanded, string accentHex) { var itemBackground = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke; var launcherBackground = TryFindResource("LauncherBackground") as Brush ?? Brushes.White; var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var expanded = _overlaySectionExpandedStates.TryGetValue(key, out var stored) ? stored : defaultExpanded; var bodyBorder = new Border { Margin = new Thickness(0, 10, 0, 0), Child = content, Visibility = expanded ? Visibility.Visible : Visibility.Collapsed }; var caret = new TextBlock { Text = expanded ? "\uE70D" : "\uE76C", FontFamily = s_segoeIconFont, FontSize = 11, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center }; var headerGrid = new Grid(); headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var headerText = new StackPanel(); headerText.Children.Add(new TextBlock { Text = title, FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = primaryText }); if (!string.IsNullOrWhiteSpace(subtitle)) { headerText.Children.Add(new TextBlock { Text = subtitle, Margin = new Thickness(0, 4, 0, 0), FontSize = 10.8, TextWrapping = TextWrapping.Wrap, Foreground = secondaryText }); } headerGrid.Children.Add(headerText); Grid.SetColumn(caret, 1); headerGrid.Children.Add(caret); var headerBorder = new Border { Background = launcherBackground, BorderBrush = BrushFromHex(accentHex), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 8, 10, 8), Cursor = Cursors.Hand, Child = headerGrid }; void Toggle() { var nextExpanded = bodyBorder.Visibility != Visibility.Visible; bodyBorder.Visibility = nextExpanded ? Visibility.Visible : Visibility.Collapsed; caret.Text = nextExpanded ? "\uE70D" : "\uE76C"; _overlaySectionExpandedStates[key] = nextExpanded; } headerBorder.MouseLeftButtonUp += (_, _) => Toggle(); return new Border { Background = itemBackground, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(10), Padding = new Thickness(10), Margin = new Thickness(0, 0, 0, 8), Child = new StackPanel { Children = { headerBorder, bodyBorder } } }; } private static TextBlock CreateOverlayPlaceholder(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 ShowOverlayHookEditDialog(AgentHookEntry? existing, int index) { var bgBrush = TryFindResource("LauncherBackground") as Brush ?? BrushFromHex("#FFFFFF"); var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.Black; var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray; var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke; 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, 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 = isNew ? "훅 추가" : "훅 편집", FontSize = 15, FontWeight = FontWeights.SemiBold, Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 14) }); dlg.KeyDown += (_, e) => { if (e.Key == 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 = CreateOverlayPlaceholder("예: 코드 리뷰 후 알림", 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 = CreateOverlayPlaceholder("예: 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()); 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 = CreateOverlayPlaceholder("예: 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); 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, Child = new TextBlock { Text = "...", FontSize = 13, Foreground = accentBrush } }; browseBtn.MouseLeftButtonUp += (_, _) => { var ofd = new 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 = CreateOverlayPlaceholder("예: --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, 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, 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 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 = _settings.Settings.Llm.AgentHooks; if (isNew) hooks.Add(entry); else if (index >= 0 && index < hooks.Count) hooks[index] = entry; BuildOverlayHookCards(); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); dlg.Close(); }; btnRow.Children.Add(saveBorder); stack.Children.Add(btnRow); border.Child = stack; dlg.Content = border; dlg.ShowDialog(); } private static string GetOverlayToolCategory(string toolName) { return toolName switch { "file_read" or "file_write" or "file_edit" or "glob" or "grep_tool" or "folder_map" or "document_read" or "file_manage" or "file_info" or "multi_read" => "파일/검색", "process" or "build_run" or "dev_env_detect" or "snippet_runner" => "프로세스/빌드", "search_codebase" or "code_search" or "code_review" or "lsp" or "test_loop" or "git_tool" or "project_rules" or "project_rule" or "diff_preview" => "코드 분석", "excel_create" or "docx_create" or "csv_create" or "markdown_create" or "html_create" or "chart_create" or "batch_create" or "pptx_create" or "document_review" or "format_convert" or "document_planner" or "document_assembler" or "template_render" => "문서 생성", "json_tool" or "regex_tool" or "diff_tool" or "base64_tool" or "hash_tool" or "datetime_tool" or "math_tool" or "xml_tool" or "sql_tool" or "data_pivot" or "text_summarize" => "데이터 처리", "clipboard_tool" or "notify_tool" or "env_tool" or "zip_tool" or "http_tool" or "open_external" or "image_analyze" or "file_watch" => "시스템/환경", "spawn_agent" or "spawn_agents" or "wait_agents" or "memory" or "skill_manager" or "user_ask" or "task_tracker" or "todo_write" or "task_create" or "task_get" or "task_list" or "task_update" or "task_stop" or "task_output" or "enter_plan_mode" or "exit_plan_mode" or "enter_worktree" or "exit_worktree" or "team_create" or "team_delete" or "cron_create" or "cron_delete" or "cron_list" or "suggest_actions" or "checkpoint" or "playbook" => "에이전트", _ => "기타" }; } private void CmbOverlayService_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (_isOverlaySettingsSyncing || CmbOverlayService.SelectedItem is not ComboBoxItem serviceItem || serviceItem.Tag is not string service) return; var llm = _settings.Settings.Llm; llm.Service = service; var candidates = GetModelCandidates(service); var preferredModel = service switch { "ollama" => llm.OllamaModel, "vllm" => llm.VllmModel, "gemini" => llm.GeminiModel, _ => llm.ClaudeModel }; if (!string.IsNullOrWhiteSpace(preferredModel)) llm.Model = preferredModel; else if (candidates.Count > 0 && !candidates.Any(m => m.Id == llm.Model)) llm.Model = candidates[0].Id; PersistOverlaySettingsState(refreshOverlayDeferredInputs: true); } private void CmbOverlayModel_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (_isOverlaySettingsSyncing || CmbOverlayModel.SelectedItem is not ComboBoxItem modelItem || modelItem.Tag is not string modelId) return; CommitOverlayModelSelection(modelId); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void RefreshOverlayThemeCards() { var selected = (_settings.Settings.Llm.AgentTheme ?? "system").ToLowerInvariant(); SetOverlayCardSelection(OverlayThemeSystemCard, selected == "system"); SetOverlayCardSelection(OverlayThemeLightCard, selected == "light"); SetOverlayCardSelection(OverlayThemeDarkCard, selected == "dark"); var preset = (_settings.Settings.Llm.AgentThemePreset ?? "claude").ToLowerInvariant(); SetOverlayCardSelection(OverlayThemeStyleClaudeCard, preset == "claude"); SetOverlayCardSelection(OverlayThemeStyleCodexCard, preset == "codex"); SetOverlayCardSelection(OverlayThemeStyleNordCard, preset == "nord"); SetOverlayCardSelection(OverlayThemeStyleEmberCard, preset == "ember"); SetOverlayCardSelection(OverlayThemeStyleSlateCard, preset == "slate"); } private void RefreshOverlayServiceCards() { var service = (_settings.Settings.Llm.Service ?? "ollama").ToLowerInvariant(); SetOverlayCardSelection(OverlaySvcOllamaCard, service == "ollama"); SetOverlayCardSelection(OverlaySvcVllmCard, service == "vllm"); SetOverlayCardSelection(OverlaySvcGeminiCard, service == "gemini"); SetOverlayCardSelection(OverlaySvcClaudeCard, service is "claude" or "sigmoid"); } private void RefreshOverlayTokenPresetCards() { var llm = _settings.Settings.Llm; var compact = llm.ContextCompactTriggerPercent switch { <= 60 => 60, <= 70 => 70, <= 80 => 80, _ => 90 }; SetOverlayCardSelection(OverlayCompact60Card, compact == 60); SetOverlayCardSelection(OverlayCompact70Card, compact == 70); SetOverlayCardSelection(OverlayCompact80Card, compact == 80); SetOverlayCardSelection(OverlayCompact90Card, compact == 90); var context = llm.MaxContextTokens switch { <= 4096 => 4096, <= 16384 => 16384, <= 32768 => 32768, <= 65536 => 65536, <= 131072 => 131072, <= 262144 => 262144, _ => 1_000_000 }; SetOverlayCardSelection(OverlayContext4KCard, context == 4096); SetOverlayCardSelection(OverlayContext16KCard, context == 16384); SetOverlayCardSelection(OverlayContext32KCard, context == 32768); SetOverlayCardSelection(OverlayContext64KCard, context == 65536); SetOverlayCardSelection(OverlayContext128KCard, context == 131072); SetOverlayCardSelection(OverlayContext256KCard, context == 262144); SetOverlayCardSelection(OverlayContext1MCard, context == 1_000_000); } private void RefreshOverlayModeButtons() { var llm = _settings.Settings.Llm; SelectComboTag(CmbOverlayOperationMode, OperationModePolicy.Normalize(_settings.Settings.OperationMode)); SelectComboTag(CmbOverlayPermission, PermissionModeCatalog.NormalizeGlobalMode(llm.FilePermission)); SelectComboTag(CmbOverlayReasoning, llm.AgentDecisionLevel); SelectComboTag(CmbOverlayFastMode, llm.FreeTierMode ? "on" : "off"); // CmbOverlayDefaultOutputFormat, CmbOverlayDefaultMood, CmbOverlayAutoPreview 제거됨 (중복 설정 항목) SelectComboTag(CmbOverlayAgentLogLevel, llm.AgentLogLevel ?? "detailed"); UpdateDataUsageUI(); RefreshOverlayTemperatureModeButtons(); } private void RefreshOverlayTemperatureModeButtons() { if (OverlayTemperatureAutoCard == null || OverlayTemperatureCustomCard == null) return; var automatic = _settings.Settings.Llm.UseAutomaticProfileTemperature; SetOverlayCardSelection(OverlayTemperatureAutoCard, automatic); SetOverlayCardSelection(OverlayTemperatureCustomCard, !automatic); if (SldOverlayTemperature != null) { SldOverlayTemperature.IsEnabled = !automatic; SldOverlayTemperature.Opacity = automatic ? 0.55 : 1.0; } if (TxtOverlayTemperatureValue != null) TxtOverlayTemperatureValue.Opacity = automatic ? 0.65 : 1.0; } private static void SelectComboTag(ComboBox? combo, string? tag) { if (combo == null) return; var normalized = (tag ?? "").Trim(); combo.SelectedItem = combo.Items .OfType() .FirstOrDefault(item => string.Equals(item.Tag as string, normalized, StringComparison.OrdinalIgnoreCase)); } private void PopulateOverlayMoodCombo() { // CmbOverlayDefaultMood 제거됨 (중복 설정 항목) } private static string GetQuickActionLabel(string title, string value) => $"{title} · {value}"; private void RefreshOverlayServiceFieldLabels(string service) { if (OverlayEndpointLabel == null || OverlayEndpointHint == null || OverlayApiKeyLabel == null || OverlayApiKeyHint == null) return; switch (service) { case "ollama": OverlayEndpointLabel.Text = "Ollama 서버 주소"; OverlayEndpointHint.Text = "사내 로컬 Ollama 기본 주소를 입력합니다."; OverlayApiKeyLabel.Text = "Ollama API 키"; OverlayApiKeyHint.Text = "사내 게이트웨이를 쓰는 경우에만 입력합니다."; break; case "vllm": OverlayEndpointLabel.Text = "vLLM 서버 주소"; OverlayEndpointHint.Text = "OpenAI 호환 엔드포인트 주소를 입력합니다."; OverlayApiKeyLabel.Text = "vLLM API 키"; OverlayApiKeyHint.Text = "사내 인증 게이트웨이를 쓰는 경우에만 입력합니다."; break; case "gemini": OverlayEndpointLabel.Text = "기본 서버 주소"; OverlayEndpointHint.Text = "Gemini는 내부 기본 주소를 사용합니다."; OverlayApiKeyLabel.Text = "Gemini API 키"; OverlayApiKeyHint.Text = "외부 호출에 필요한 키를 입력합니다."; break; default: OverlayEndpointLabel.Text = "기본 서버 주소"; OverlayEndpointHint.Text = "Claude는 내부 기본 주소를 사용합니다."; OverlayApiKeyLabel.Text = "Claude API 키"; OverlayApiKeyHint.Text = "외부 호출에 필요한 키를 입력합니다."; break; } } private void RefreshOverlayServiceFieldVisibility(string service) { if (OverlayEndpointFieldPanel == null || OverlayApiKeyFieldPanel == null) return; var hideEndpoint = string.Equals(service, "gemini", StringComparison.OrdinalIgnoreCase) || string.Equals(service, "claude", StringComparison.OrdinalIgnoreCase); OverlayEndpointFieldPanel.Visibility = hideEndpoint ? Visibility.Collapsed : Visibility.Visible; OverlayApiKeyFieldPanel.Margin = hideEndpoint ? new Thickness(0) : new Thickness(6, 0, 0, 0); Grid.SetColumn(OverlayApiKeyFieldPanel, hideEndpoint ? 0 : 1); Grid.SetColumnSpan(OverlayApiKeyFieldPanel, hideEndpoint ? 2 : 1); } private string GetOverlayServiceEndpoint(string service) { var llm = _settings.Settings.Llm; return service switch { "ollama" => llm.OllamaEndpoint ?? "", "vllm" => llm.VllmEndpoint ?? "", "gemini" => llm.Endpoint ?? "", "claude" or "sigmoid" => llm.Endpoint ?? "", _ => llm.Endpoint ?? "" }; } private string GetOverlayServiceApiKey(string service) { var llm = _settings.Settings.Llm; return service switch { "ollama" => llm.OllamaApiKey ?? "", "vllm" => llm.VllmApiKey ?? "", "gemini" => llm.GeminiApiKey ?? "", "claude" or "sigmoid" => llm.ClaudeApiKey ?? "", _ => llm.ApiKey ?? "" }; } private void BuildOverlayRegisteredModelsPanel(string service) { if (OverlayRegisteredModelsPanel == null || OverlayRegisteredModelsHeader == null || BtnOverlayAddModel == null) return; var normalized = NormalizeOverlayService(service); var supportsRegistered = SupportsOverlayRegisteredModels(normalized); OverlayRegisteredModelsHeader.Visibility = supportsRegistered ? Visibility.Visible : Visibility.Collapsed; OverlayRegisteredModelsPanel.Visibility = supportsRegistered ? Visibility.Visible : Visibility.Collapsed; BtnOverlayAddModel.Visibility = supportsRegistered ? Visibility.Visible : Visibility.Collapsed; OverlayRegisteredModelsPanel.Children.Clear(); if (!supportsRegistered) return; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.White; var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? BrushFromHex("#F8FAFC"); var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue; var models = _settings.Settings.Llm.RegisteredModels .Where(m => string.Equals(m.Service, normalized, StringComparison.OrdinalIgnoreCase)) .OrderBy(m => m.Alias, StringComparer.OrdinalIgnoreCase) .ToList(); if (models.Count == 0) { OverlayRegisteredModelsPanel.Children.Add(new Border { CornerRadius = new CornerRadius(10), BorderBrush = borderBrush, BorderThickness = new Thickness(1), Background = Brushes.Transparent, Padding = new Thickness(12, 10, 12, 10), Child = new TextBlock { Text = "등록된 모델이 없습니다. `모델 추가`로 사내 모델을 먼저 등록하세요.", FontSize = 11.5, TextWrapping = TextWrapping.Wrap, Foreground = secondaryText, } }); return; } foreach (var model in models) { var decryptedModelName = Services.CryptoService.DecryptIfEnabled(model.EncryptedModelName, IsOverlayEncryptionEnabled); var displayName = string.IsNullOrWhiteSpace(model.Alias) ? decryptedModelName : model.Alias; var endpointText = string.IsNullOrWhiteSpace(model.Endpoint) ? "기본 서버 사용" : model.Endpoint; var authLabel = (model.AuthType ?? "bearer").ToLowerInvariant() switch { "cp4d" => "CP4D", "ibm_iam" => "IBM IAM", _ => "Bearer", }; var isActive = string.Equals(model.EncryptedModelName, _settings.Settings.Llm.Model, StringComparison.OrdinalIgnoreCase) || string.Equals(decryptedModelName, _settings.Settings.Llm.Model, StringComparison.OrdinalIgnoreCase); var row = new Border { CornerRadius = new CornerRadius(10), BorderBrush = isActive ? accentBrush : borderBrush, BorderThickness = new Thickness(1), Background = isActive ? (TryFindResource("HintBackground") as Brush ?? BrushFromHex("#EFF6FF")) : itemBg, Padding = new Thickness(12, 10, 12, 10), Margin = new Thickness(0, 0, 0, 8), }; 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 = displayName, FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, }); info.Children.Add(new TextBlock { Text = string.IsNullOrWhiteSpace(decryptedModelName) ? "(모델명 없음)" : decryptedModelName, Margin = new Thickness(0, 3, 0, 0), FontSize = 11, Foreground = secondaryText, }); info.Children.Add(new TextBlock { Text = $"엔드포인트: {endpointText} · 인증: {authLabel}", Margin = new Thickness(0, 4, 0, 0), FontSize = 10.5, TextWrapping = TextWrapping.Wrap, Foreground = secondaryText, }); Grid.SetColumn(info, 0); grid.Children.Add(info); var actions = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Top, }; Border CreateAction(string text, Action onClick, Brush foreground) { var label = new TextBlock { Text = text, FontSize = 11.5, FontWeight = FontWeights.SemiBold, Foreground = foreground, VerticalAlignment = VerticalAlignment.Center, }; var action = new Border { Cursor = Cursors.Hand, CornerRadius = new CornerRadius(8), Padding = new Thickness(8, 4, 8, 4), Margin = new Thickness(6, 0, 0, 0), Background = Brushes.Transparent, Child = label, }; action.MouseEnter += (_, _) => { action.Background = hoverBg; }; action.MouseLeave += (_, _) => { action.Background = Brushes.Transparent; }; action.MouseLeftButtonUp += (_, _) => onClick(); return action; } actions.Children.Add(CreateAction("선택", () => { CommitOverlayModelSelection(model.EncryptedModelName); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); }, accentBrush)); actions.Children.Add(CreateAction("편집", () => { EditOverlayRegisteredModel(model); BuildOverlayRegisteredModelsPanel(service); }, primaryText)); actions.Children.Add(CreateAction("삭제", () => { DeleteOverlayRegisteredModel(model); }, BrushFromHex("#DC2626"))); Grid.SetColumn(actions, 1); grid.Children.Add(actions); row.Child = grid; row.MouseEnter += (_, _) => { if (!isActive) row.Background = hoverBg; }; row.MouseLeave += (_, _) => { if (!isActive) row.Background = itemBg; }; OverlayRegisteredModelsPanel.Children.Add(row); } } private void BtnOverlayAddModel_Click(object sender, RoutedEventArgs e) { var service = NormalizeOverlayService(_settings.Settings.Llm.Service); if (!SupportsOverlayRegisteredModels(service)) return; var dlg = new ModelRegistrationDialog(service) { Owner = this }; if (dlg.ShowDialog() != true) return; _settings.Settings.Llm.RegisteredModels.Add(new RegisteredModel { Alias = dlg.ModelAlias, EncryptedModelName = Services.CryptoService.EncryptIfEnabled(dlg.ModelName, IsOverlayEncryptionEnabled), Service = service, ExecutionProfile = dlg.ExecutionProfile, Endpoint = dlg.Endpoint, ApiKey = dlg.ApiKey, AllowInsecureTls = dlg.AllowInsecureTls, AuthType = dlg.AuthType, Cp4dUrl = dlg.Cp4dUrl, Cp4dUsername = dlg.Cp4dUsername, Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsOverlayEncryptionEnabled), }); PersistOverlaySettingsState(refreshOverlayDeferredInputs: true); } private void EditOverlayRegisteredModel(RegisteredModel model) { var currentModel = Services.CryptoService.DecryptIfEnabled(model.EncryptedModelName, IsOverlayEncryptionEnabled); var cp4dPassword = Services.CryptoService.DecryptIfEnabled(model.Cp4dPassword ?? "", IsOverlayEncryptionEnabled); var service = NormalizeOverlayService(model.Service); var dlg = new ModelRegistrationDialog( service, model.Alias, currentModel, model.Endpoint, model.ApiKey, model.AllowInsecureTls, model.AuthType ?? "bearer", model.Cp4dUrl ?? "", model.Cp4dUsername ?? "", cp4dPassword, model.ExecutionProfile ?? "balanced") { Owner = this }; if (dlg.ShowDialog() != true) return; model.Alias = dlg.ModelAlias; model.EncryptedModelName = Services.CryptoService.EncryptIfEnabled(dlg.ModelName, IsOverlayEncryptionEnabled); model.Service = service; model.ExecutionProfile = dlg.ExecutionProfile; model.Endpoint = dlg.Endpoint; model.ApiKey = dlg.ApiKey; model.AllowInsecureTls = dlg.AllowInsecureTls; model.AuthType = dlg.AuthType; model.Cp4dUrl = dlg.Cp4dUrl; model.Cp4dUsername = dlg.Cp4dUsername; model.Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsOverlayEncryptionEnabled); PersistOverlaySettingsState(refreshOverlayDeferredInputs: true); } private void DeleteOverlayRegisteredModel(RegisteredModel model) { var result = CustomMessageBox.Show($"'{model.Alias}' 모델을 삭제하시겠습니까?", "모델 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result != MessageBoxResult.Yes) return; _settings.Settings.Llm.RegisteredModels.Remove(model); PersistOverlaySettingsState(refreshOverlayDeferredInputs: true); } private void SetOverlayCardSelection(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 OverlayThemeSystemCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _settings.Settings.Llm.AgentTheme = "system"; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void OverlayThemeLightCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _settings.Settings.Llm.AgentTheme = "light"; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void OverlayThemeDarkCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _settings.Settings.Llm.AgentTheme = "dark"; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void OverlayThemeStyleClaudeCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _settings.Settings.Llm.AgentThemePreset = "claude"; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void OverlayThemeStyleCodexCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _settings.Settings.Llm.AgentThemePreset = "codex"; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void OverlayThemeStyleNordCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _settings.Settings.Llm.AgentThemePreset = "nord"; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void OverlayThemeStyleEmberCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _settings.Settings.Llm.AgentThemePreset = "ember"; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void OverlayThemeStyleSlateCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { _settings.Settings.Llm.AgentThemePreset = "slate"; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void OverlaySvcOllamaCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetOverlayService("ollama"); private void OverlaySvcVllmCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetOverlayService("vllm"); private void OverlaySvcGeminiCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetOverlayService("gemini"); private void OverlaySvcClaudeCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetOverlayService("claude"); private void SetOverlayService(string service) { _settings.Settings.Llm.Service = service; var llm = _settings.Settings.Llm; var candidates = GetModelCandidates(service); var preferredModel = service switch { "ollama" => llm.OllamaModel, "vllm" => llm.VllmModel, "gemini" => llm.GeminiModel, _ => llm.ClaudeModel }; llm.Model = !string.IsNullOrWhiteSpace(preferredModel) ? preferredModel : candidates.FirstOrDefault().Id ?? llm.Model; PersistOverlaySettingsState(refreshOverlayDeferredInputs: true); RefreshOverlayVisualState(loadDeferredInputs: true); } private void BtnOverlayOperationMode_Click(object sender, RoutedEventArgs e) { var next = OperationModePolicy.Normalize(_settings.Settings.OperationMode) == OperationModePolicy.ExternalMode ? OperationModePolicy.InternalMode : OperationModePolicy.ExternalMode; if (!PromptOverlayPasswordDialog("운영 모드 변경", "사내/사외 모드 변경", "비밀번호를 입력하세요.")) return; _settings.Settings.OperationMode = next; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void BtnOverlayFolderDataUsage_Click(object sender, RoutedEventArgs e) { _folderDataUsage = GetAutomaticFolderDataUsage(); } private void CmbOverlayFastMode_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (_isOverlaySettingsSyncing || CmbOverlayFastMode.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag) return; _settings.Settings.Llm.FreeTierMode = string.Equals(tag, "on", StringComparison.OrdinalIgnoreCase); PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void CmbOverlayReasoning_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (_isOverlaySettingsSyncing || CmbOverlayReasoning.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag) return; _settings.Settings.Llm.AgentDecisionLevel = tag; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void CmbOverlayPermission_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (_isOverlaySettingsSyncing || CmbOverlayPermission.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag) return; var normalized = PermissionModeCatalog.NormalizeGlobalMode(tag); var llm = _settings.Settings.Llm; llm.FilePermission = normalized; llm.DefaultAgentPermission = normalized; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void CmbOverlayDefaultOutputFormat_SelectionChanged(object sender, SelectionChangedEventArgs e) { // 제거됨 (중복 설정 항목) } private void CmbOverlayDefaultMood_SelectionChanged(object sender, SelectionChangedEventArgs e) { // 제거됨 (중복 설정 항목) } private void CmbOverlayAutoPreview_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (_isOverlaySettingsSyncing) return; ApplyOverlaySettingsChanges(showToast: false, closeOverlay: false); } private void CmbOverlayOperationMode_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (_isOverlaySettingsSyncing || CmbOverlayOperationMode.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag) return; var normalized = OperationModePolicy.Normalize(tag); var current = OperationModePolicy.Normalize(_settings.Settings.OperationMode); if (string.Equals(normalized, current, StringComparison.OrdinalIgnoreCase)) return; if (string.Equals(normalized, OperationModePolicy.ExternalMode, StringComparison.OrdinalIgnoreCase) && !PromptOverlayPasswordDialog("운영 모드 변경", "사내/사외 모드 변경", "비밀번호를 입력하세요.")) { RefreshOverlayModeButtons(); return; } _settings.Settings.OperationMode = normalized; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void CmbOverlayFolderDataUsage_SelectionChanged(object sender, SelectionChangedEventArgs e) { _folderDataUsage = GetAutomaticFolderDataUsage(); } private void CmbOverlayAgentLogLevel_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (_isOverlaySettingsSyncing || CmbOverlayAgentLogLevel.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag) return; _settings.Settings.Llm.AgentLogLevel = tag; PersistOverlaySettingsState(refreshOverlayDeferredInputs: false); } private void BtnOpenFullSettings_Click(object sender, RoutedEventArgs e) { if (System.Windows.Application.Current is App app) app.OpenSettingsFromChat(); } private bool PromptOverlayPasswordDialog(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 static int ParseOverlayInt(string? text, int fallback, int min, int max) { if (!int.TryParse(text, out var value)) value = fallback; return Math.Clamp(value, min, max); } private void BtnInlineSettingsClose_Click(object sender, RoutedEventArgs e) => InlineSettingsPanel.IsOpen = false; private void CmbInlineService_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (_isInlineSettingsSyncing || CmbInlineService.SelectedItem is not ComboBoxItem serviceItem || serviceItem.Tag is not string service) return; var llm = _settings.Settings.Llm; llm.Service = service; var candidates = GetModelCandidates(service); if (candidates.Count > 0 && !candidates.Any(m => m.Id == llm.Model)) llm.Model = candidates[0].Id; ScheduleSettingsSave(); _appState.LoadFromSettings(_settings); UpdateModelLabel(); RefreshInlineSettingsPanel(); } private void CmbInlineModel_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (_isInlineSettingsSyncing || CmbInlineModel.SelectedItem is not ComboBoxItem modelItem || modelItem.Tag is not string modelId) return; _settings.Settings.Llm.Model = modelId; ScheduleSettingsSave(); _appState.LoadFromSettings(_settings); UpdateModelLabel(); RefreshInlineSettingsPanel(); } private void BtnInlineFastMode_Click(object sender, RoutedEventArgs e) { _settings.Settings.Llm.FreeTierMode = !_settings.Settings.Llm.FreeTierMode; ScheduleSettingsSave(); _appState.LoadFromSettings(_settings); RefreshInlineSettingsPanel(); RefreshOverlayVisualState(loadDeferredInputs: false); } private void BtnInlineReasoning_Click(object sender, RoutedEventArgs e) { var llm = _settings.Settings.Llm; llm.AgentDecisionLevel = NextReasoning(llm.AgentDecisionLevel); ScheduleSettingsSave(); _appState.LoadFromSettings(_settings); RefreshInlineSettingsPanel(); RefreshOverlayVisualState(loadDeferredInputs: false); } private void BtnInlinePermission_Click(object sender, RoutedEventArgs e) { var llm = _settings.Settings.Llm; llm.FilePermission = NextPermission(llm.FilePermission); ScheduleSettingsSave(); _appState.LoadFromSettings(_settings); UpdatePermissionUI(); SaveConversationSettings(); RefreshInlineSettingsPanel(); RefreshOverlayVisualState(loadDeferredInputs: false); } private void BtnInlineSkill_Click(object sender, RoutedEventArgs e) { var llm = _settings.Settings.Llm; llm.EnableSkillSystem = !llm.EnableSkillSystem; if (llm.EnableSkillSystem) { SkillService.EnsureSkillFolder(); SkillService.LoadSkills(llm.SkillsFolderPath, GetCurrentWorkFolder()); UpdateConditionalSkillActivation(reset: true); } ScheduleSettingsSave(); _appState.LoadFromSettings(_settings); RefreshInlineSettingsPanel(); if (llm.EnableSkillSystem) OpenCommandSkillBrowser("/"); } private void BtnInlineCommandBrowser_Click(object sender, RoutedEventArgs e) => OpenCommandSkillBrowser("/"); private void BtnInlineMcp_Click(object sender, RoutedEventArgs e) { var app = System.Windows.Application.Current as App; app?.OpenSettingsFromChat(); } private void BtnNewChat_Click(object sender, RoutedEventArgs e) { StartNewConversation(); InputBox.Focus(); } public void ResumeConversation(string conversationId) { var conv = _storage.Load(conversationId); if (conv != null) { var targetTab = NormalizeTabName(conv.Tab); if (!string.Equals(_activeTab, targetTab, StringComparison.OrdinalIgnoreCase)) { SaveCurrentTabConversationId(); PersistPerTabUiState(); _activeTab = targetTab; RestorePerTabUiState(); UpdateTabUI(); if (string.Equals(targetTab, "Chat", StringComparison.OrdinalIgnoreCase)) TabChat.IsChecked = true; else if (string.Equals(targetTab, "Cowork", StringComparison.OrdinalIgnoreCase)) TabCowork.IsChecked = true; else if (TabCode.IsEnabled) TabCode.IsChecked = true; } lock (_convLock) { conv.Tab = targetTab; _currentConversation = ChatSession?.SetCurrentConversation(targetTab, conv, _storage) ?? conv; SyncTabConversationIdsFromSession(); } SaveLastConversations(); UpdateChatTitle(); RefreshConversationList(); RenderMessages(); UpdateFolderBar(); RefreshDraftQueueUi(); } InputBox.Focus(); } private static string NormalizeTabName(string? tab) { var normalized = (tab ?? "").Trim(); if (string.IsNullOrEmpty(normalized)) return "Chat"; if (normalized.Contains("코워크", StringComparison.OrdinalIgnoreCase)) return "Cowork"; var canonical = new string(normalized .Where(char.IsLetterOrDigit) .ToArray()) .ToLowerInvariant(); if (canonical is "cowork" or "coworkcode" or "coworkcodetab") return "Cowork"; if (normalized.Contains("코드", StringComparison.OrdinalIgnoreCase) || canonical is "code" or "codetab") return "Code"; return "Chat"; } public void StartNewAndFocus() { StartNewConversation(); InputBox.Focus(); } private void BtnDeleteAll_Click(object sender, RoutedEventArgs e) { var tabLabel = _activeTab switch { "Cowork" => "코워크", "Code" => "코드", _ => "채팅" }; var result = CustomMessageBox.Show( $"'{tabLabel}' 탭의 모든 대화 내역을 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.", $"{tabLabel} 대화 전체 삭제", MessageBoxButton.YesNo, MessageBoxImage.Warning); if (result != MessageBoxResult.Yes) return; _storage.DeleteAllByTab(_activeTab); lock (_convLock) { ChatSession?.ClearCurrentConversation(_activeTab); _currentConversation = null; SyncTabConversationIdsFromSession(); } ClearTranscriptElements(); EmptyState.Visibility = Visibility.Visible; UpdateChatTitle(); RefreshConversationList(); } }