Files
AX-Copilot-Codex/src/AxCopilot/Views/AgentSettingsWindow.xaml.cs
lacvet a0ce5846e1
Some checks failed
Release Gate / gate (push) Has been cancelled
AX Agent 구형 설정창의 PlanMode dead UI를 제거
- AgentSettingsWindow에서 계획 모드와 Plan Mode 도구 UI 및 save/load/event 코드 제거

- clean 파일 기준 검색상 남은 PlanMode 참조를 JSON 호환용 AppSettings와 SubAgentTool 안전 고정 경로로 축소

- README와 DEVELOPMENT 문서에 2026-04-05 21:20 (KST) 기준 변경 내역과 parity 99% 재평가 반영

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-05 18:12:31 +09:00

1126 lines
48 KiB
C#

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