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