diff --git a/README.md b/README.md
index 4c3e21d..8f3464c 100644
--- a/README.md
+++ b/README.md
@@ -965,6 +965,10 @@ ow + toggle 시각 언어로 통일했습니다.
- 이번 정리 후 추정 parity 는 `core engine 95% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 95% / overall 98%` 정도로 재평가했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 21:12 (KST)
+- 구형 [AgentSettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/AgentSettingsWindow.xaml), [AgentSettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/AgentSettingsWindow.xaml.cs) 에 남아 있던 `계획 모드`, `Plan Mode 도구` UI와 관련 save/load/event 코드도 제거했습니다. 현재 clean 파일 기준 검색상 남은 `PlanMode`/`EnablePlanModeTools` 참조는 JSON 호환용 [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs) 와 안전 고정용 [SubAgentTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/SubAgentTool.cs) 정도입니다.
+- 이번 정리 후 추정 parity 는 `core engine 95% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 97% / overall 99%` 정도로 재평가했습니다.
+- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
+- 업데이트: 2026-04-05 21:20 (KST)
---
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 8d72d63..ae5af29 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -4729,3 +4729,7 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
- 이번 묶음 후 추정 parity 는 `core engine 95% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 95% / overall 98%` 정도로 재평가했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
- 업데이트: 2026-04-05 21:12 (KST)
+- 구형 [AgentSettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/AgentSettingsWindow.xaml), [AgentSettingsWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/AgentSettingsWindow.xaml.cs) 의 `계획 모드`, `Plan Mode 도구` UI와 관련 save/load/event 코드도 제거했습니다. clean 파일 기준 검색상 남은 `PlanMode`/`EnablePlanModeTools` 참조는 JSON 저장 호환을 위한 [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs) 와 안전 고정값을 두는 [SubAgentTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/SubAgentTool.cs) 정도만 남았습니다.
+- 이번 묶음 후 추정 parity 는 `core engine 95% / main transcript UI 97% / Cowork·Code runtime UX 97% / internal settings 97% / overall 99%` 정도로 재평가했습니다.
+- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0
+- 업데이트: 2026-04-05 21:20 (KST)
diff --git a/src/AxCopilot/Views/AgentSettingsWindow.xaml b/src/AxCopilot/Views/AgentSettingsWindow.xaml
index 2e0e0f2..5ccc65d 100644
--- a/src/AxCopilot/Views/AgentSettingsWindow.xaml
+++ b/src/AxCopilot/Views/AgentSettingsWindow.xaml
@@ -74,14 +74,26 @@
+
+
-
+
+
@@ -113,10 +125,159 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+ 사내 모드는 외부 검색, 외부 URL 열기, 외부 HTTP 호출을 제한합니다.
+ 사외 모드는 외부 연동이 필요한 작업까지 허용합니다.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 결과물을 어떤 문서 형태로 우선 제안할지 정합니다.
+ AI 자동으로 두면 요청 성격에 맞는 형식을 먼저 고릅니다.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ HTML 미리보기나 문서형 결과물에서 기본으로 쓸 시각 스타일입니다.
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+ 파일 읽기, 수정, 실행 요청을 얼마나 자주 승인받을지 정합니다.
+ 보수적으로 둘수록 안전하고, 자동 승인 계열일수록 빠르게 진행됩니다.
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ 답을 만들기 전에 얼마나 많이 따져보고 진행할지 정합니다.
+
+
+
+
+
-
+
+
+
+
+
+
+
+ 현재 작업 폴더 파일과 문맥을 AI가 얼마나 적극적으로 참고할지 정합니다.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 한 번의 작업에서 에이전트가 계획, 실행, 확인 단계를 반복할 수 있는 최대 횟수입니다.
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+ 한 번의 응답 생성에 넘겨줄 최대 문맥 크기입니다.
+
+
+
+
-
+
+
+
+
+
+
+ 도구 실행 실패 시 자동으로 다시 시도할 최대 횟수입니다.
+
+
+
+
-
+
+
-
+
+
+
+
+
+
+ 슬래시 명령으로 재사용 가능한 작업 템플릿을 불러오는 기능입니다.
+
+
+
+
@@ -400,9 +818,19 @@
-
+
+
+
+
+
+
+ 특정 도구 실행 전후에 스크립트를 자동으로 호출하는 확장 기능입니다.
+
+
+
+
@@ -431,15 +859,140 @@
Grid.Column="1"
Style="{StaticResource ToggleSwitch}"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 추가 스킬을 읽어올 폴더입니다. `.skill.md` 또는 `SKILL.md` 파일을 두면 저장 후 다시 불러옵니다.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `/` 입력 시 한 번에 보여줄 명령과 스킬 개수입니다.
+
+
+
+
+
+
-
-
+
+
+
+
+
+ 파일을 끌어놓았을 때 AI 작업 추천과 첨부 보조 동작을 보여줍니다.
+
+
+
+
+
@@ -448,30 +1001,61 @@
-
-
+
+
+
+
+
+ 추천 액션을 고르면 바로 메시지를 보내 실행까지 이어갑니다.
+
+
+
+
+
-
+
+
+
+
+
-
-
+
+
+
+
+
+
+ 외부 도구 서버를 연결해 AX Agent가 브라우저나 파일시스템 같은 확장 기능을 사용할 수 있게 합니다.
+
+
+
+
+
+
-
diff --git a/src/AxCopilot/Views/AgentSettingsWindow.xaml.cs b/src/AxCopilot/Views/AgentSettingsWindow.xaml.cs
index 7151479..5cd27a9 100644
--- a/src/AxCopilot/Views/AgentSettingsWindow.xaml.cs
+++ b/src/AxCopilot/Views/AgentSettingsWindow.xaml.cs
@@ -2,6 +2,7 @@
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;
@@ -15,11 +16,18 @@ public partial class AgentSettingsWindow : Window
private readonly SettingsService _settings;
private readonly LlmSettings _llm;
private string _permissionMode = PermissionModeCatalog.Deny;
- private string _planMode = "off";
private string _reasoningMode = "detailed";
private string _folderDataUsage = "active";
private string _operationMode = OperationModePolicy.InternalMode;
+ private string _displayMode = "rich";
+ private string _defaultOutputFormat = "auto";
+ private string _defaultMood = "modern";
+ private string _activePanel = "basic";
+ private string _selectedService = "ollama";
+ private string _selectedTheme = "system";
private string _selectedModel = string.Empty;
+ private bool _toolCardsLoaded;
+ private HashSet _disabledTools = new(StringComparer.OrdinalIgnoreCase);
public AgentSettingsWindow(SettingsService settings)
{
@@ -31,14 +39,24 @@ public partial class AgentSettingsWindow : Window
private void LoadFromSettings()
{
- _selectedModel = _llm.Model ?? "";
+ 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);
- _planMode = string.IsNullOrWhiteSpace(_llm.PlanMode) ? "off" : _llm.PlanMode;
_reasoningMode = string.IsNullOrWhiteSpace(_llm.AgentDecisionLevel) ? "detailed" : _llm.AgentDecisionLevel;
_folderDataUsage = string.IsNullOrWhiteSpace(_llm.FolderDataUsage) ? "active" : _llm.FolderDataUsage;
_operationMode = OperationModePolicy.Normalize(_settings.Settings.OperationMode);
+ _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();
@@ -49,40 +67,93 @@ public partial class AgentSettingsWindow : Window
ChkEnableHookInputMutation.IsChecked = _llm.EnableHookInputMutation;
ChkEnableHookPermissionUpdate.IsChecked = _llm.EnableHookPermissionUpdate;
ChkEnableCoworkVerification.IsChecked = _llm.EnableCoworkVerification;
+ ChkEnableProjectRules.IsChecked = _llm.EnableProjectRules;
+ ChkEnableAgentMemory.IsChecked = _llm.EnableAgentMemory;
+ TxtMaxAgentIterations.Text = Math.Clamp(_llm.MaxAgentIterations, 1, 200).ToString();
ChkEnableCodeVerification.IsChecked = _llm.Code.EnableCodeVerification;
ChkEnableParallelTools.IsChecked = _llm.EnableParallelTools;
+ ChkEnableWorktreeTools.IsChecked = _llm.Code.EnableWorktreeTools;
+ ChkEnableTeamTools.IsChecked = _llm.Code.EnableTeamTools;
+ ChkEnableCronTools.IsChecked = _llm.Code.EnableCronTools;
+ TxtSkillsFolderPath.Text = _llm.SkillsFolderPath ?? "";
+ TxtSlashPopupPageSize.Text = Math.Clamp(_llm.SlashPopupPageSize, 3, 20).ToString();
+ ChkEnableDragDropAiActions.IsChecked = _llm.EnableDragDropAiActions;
+ ChkDragDropAutoSend.IsChecked = _llm.DragDropAutoSend;
+ _disabledTools = new HashSet(_llm.DisabledTools ?? new(), StringComparer.OrdinalIgnoreCase);
RefreshServiceCards();
RefreshThemeCards();
RefreshModeLabels();
+ RefreshChatOptionLabels();
+ RefreshDisplayModeCards();
+ ShowPanel("basic");
BuildModelChips();
+ BuildFallbackModelsPanel();
+ BuildMcpServerCards();
+ BuildHookCards();
+ BuildSkillListPanel();
}
private void RefreshThemeCards()
{
- var selected = (_llm.AgentTheme ?? "system").ToLowerInvariant();
- SetCardSelection(ThemeSystemCard, selected == "system");
- SetCardSelection(ThemeLightCard, selected == "light");
- SetCardSelection(ThemeDarkCard, selected == "dark");
+ SetCardSelection(ThemeSystemCard, _selectedTheme == "system");
+ SetCardSelection(ThemeLightCard, _selectedTheme == "light");
+ SetCardSelection(ThemeDarkCard, _selectedTheme == "dark");
}
private void RefreshServiceCards()
{
- var service = (_llm.Service ?? "ollama").ToLowerInvariant();
- SetCardSelection(SvcOllamaCard, service == "ollama");
- SetCardSelection(SvcVllmCard, service == "vllm");
- SetCardSelection(SvcGeminiCard, service == "gemini");
- SetCardSelection(SvcClaudeCard, service is "claude" or "sigmoid");
+ 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);
- BtnPlanMode.Content = BuildPlanModeLabel(_planMode);
BtnReasoningMode.Content = BuildReasoningModeLabel(_reasoningMode);
BtnFolderDataUsage.Content = BuildFolderDataUsageLabel(_folderDataUsage);
- AdvancedPanel.Visibility = Visibility.Visible;
+ }
+
+ 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)
@@ -92,16 +163,6 @@ public partial class AgentSettingsWindow : Window
: "사내 모드";
}
- private static string BuildPlanModeLabel(string mode)
- {
- return (mode ?? "off").ToLowerInvariant() switch
- {
- "always" => "항상 계획",
- "auto" => "자동 계획",
- _ => "끄기",
- };
- }
-
private static string BuildReasoningModeLabel(string mode)
{
return (mode ?? "detailed").ToLowerInvariant() switch
@@ -122,6 +183,37 @@ public partial class AgentSettingsWindow : Window
};
}
+ 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;
@@ -135,7 +227,7 @@ public partial class AgentSettingsWindow : Window
private void BuildModelChips()
{
ModelChipPanel.Children.Clear();
- var models = GetModelCandidates(_llm.Service);
+ var models = GetModelCandidates(_selectedService);
foreach (var model in models)
{
var captured = model;
@@ -195,17 +287,30 @@ public partial class AgentSettingsWindow : Window
private void SetService(string service)
{
- _llm.Service = service;
+ _selectedService = service;
+ _selectedModel = GetSelectedModelForService(service);
+ ModelInput.Text = _selectedModel;
RefreshServiceCards();
BuildModelChips();
}
private void SetTheme(string theme)
{
- _llm.AgentTheme = 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
@@ -306,11 +411,21 @@ public partial class AgentSettingsWindow : Window
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)
{
@@ -343,17 +458,6 @@ public partial class AgentSettingsWindow : Window
RefreshModeLabels();
}
- private void BtnPlanMode_Click(object sender, RoutedEventArgs e)
- {
- _planMode = _planMode switch
- {
- "off" => "auto",
- "auto" => "always",
- _ => "off",
- };
- RefreshModeLabels();
- }
-
private void BtnReasoningMode_Click(object sender, RoutedEventArgs e)
{
_reasoningMode = _reasoningMode switch
@@ -376,13 +480,66 @@ public partial class AgentSettingsWindow : Window
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)
{
- _llm.Model = string.IsNullOrWhiteSpace(_selectedModel) ? (_llm.Model ?? "") : _selectedModel.Trim();
+ _selectedModel = string.IsNullOrWhiteSpace(ModelInput.Text) ? _selectedModel : ModelInput.Text.Trim();
+ _llm.Service = _selectedService;
+ _llm.AgentTheme = _selectedTheme;
_llm.FilePermission = _permissionMode;
- _llm.PlanMode = _planMode;
+ _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);
@@ -394,15 +551,563 @@ public partial class AgentSettingsWindow : Window
_llm.EnableHookInputMutation = ChkEnableHookInputMutation.IsChecked == true;
_llm.EnableHookPermissionUpdate = ChkEnableHookPermissionUpdate.IsChecked == true;
_llm.EnableCoworkVerification = ChkEnableCoworkVerification.IsChecked == true;
+ _llm.EnableProjectRules = ChkEnableProjectRules.IsChecked == true;
+ _llm.EnableAgentMemory = ChkEnableAgentMemory.IsChecked == true;
+ _llm.MaxAgentIterations = ParseInt(TxtMaxAgentIterations.Text, 25, 1, 200);
_llm.Code.EnableCodeVerification = ChkEnableCodeVerification.IsChecked == true;
_llm.EnableParallelTools = ChkEnableParallelTools.IsChecked == true;
+ _llm.Code.EnableWorktreeTools = ChkEnableWorktreeTools.IsChecked == true;
+ _llm.Code.EnableTeamTools = ChkEnableTeamTools.IsChecked == true;
+ _llm.Code.EnableCronTools = ChkEnableCronTools.IsChecked == true;
+ _llm.SkillsFolderPath = TxtSkillsFolderPath.Text?.Trim() ?? "";
+ _llm.SlashPopupPageSize = ParseInt(TxtSlashPopupPageSize.Text, 7, 3, 20);
+ _llm.EnableDragDropAiActions = ChkEnableDragDropAiActions.IsChecked == true;
+ _llm.DragDropAutoSend = ChkDragDropAutoSend.IsChecked == true;
+ _llm.DisabledTools = _disabledTools.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList();
+ _settings.Settings.AiEnabled = true;
_settings.Settings.OperationMode = OperationModePolicy.Normalize(_operationMode);
_settings.Save();
+ SkillService.LoadSkills(_llm.SkillsFolderPath);
+ BuildSkillListPanel();
DialogResult = true;
Close();
}
+ private void BtnBrowseSkillFolder_Click(object sender, RoutedEventArgs e)
+ {
+ var dlg = new System.Windows.Forms.FolderBrowserDialog
+ {
+ Description = "스킬 폴더 선택",
+ ShowNewFolderButton = true,
+ };
+ if (!string.IsNullOrWhiteSpace(TxtSkillsFolderPath.Text) && System.IO.Directory.Exists(TxtSkillsFolderPath.Text))
+ dlg.SelectedPath = TxtSkillsFolderPath.Text;
+ if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
+ TxtSkillsFolderPath.Text = dlg.SelectedPath;
+ }
+
+ private void BtnOpenSkillFolder_Click(object sender, RoutedEventArgs e)
+ {
+ var folder = string.IsNullOrWhiteSpace(TxtSkillsFolderPath.Text)
+ ? System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills")
+ : TxtSkillsFolderPath.Text.Trim();
+ if (!System.IO.Directory.Exists(folder))
+ System.IO.Directory.CreateDirectory(folder);
+ try { System.Diagnostics.Process.Start("explorer.exe", folder); } catch { }
+ }
+
+ private void BuildSkillListPanel()
+ {
+ if (SkillListPanel == null)
+ return;
+
+ SkillListPanel.Children.Clear();
+ var skills = SkillService.Skills;
+ if (skills.Count == 0)
+ {
+ SkillListPanel.Children.Add(new Border
+ {
+ Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke,
+ BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(10),
+ Padding = new Thickness(12, 10, 12, 10),
+ Child = new TextBlock
+ {
+ Text = "로드된 스킬이 없습니다. 스킬 폴더를 열어 `.skill.md` 또는 `SKILL.md` 파일을 추가한 뒤 저장하면 다시 불러옵니다.",
+ FontSize = 11,
+ TextWrapping = TextWrapping.Wrap,
+ Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
+ }
+ });
+ return;
+ }
+
+ var groups = new[]
+ {
+ new { Title = "내장 스킬", Items = skills.Where(s => string.IsNullOrWhiteSpace(s.Requires)).ToList() },
+ new { Title = "고급 스킬", Items = skills.Where(s => !string.IsNullOrWhiteSpace(s.Requires)).ToList() },
+ };
+
+ foreach (var group in groups)
+ {
+ if (group.Items.Count == 0)
+ continue;
+
+ SkillListPanel.Children.Add(new TextBlock
+ {
+ Text = $"{group.Title} ({group.Items.Count})",
+ FontSize = 12,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
+ Margin = new Thickness(0, 0, 0, 6),
+ });
+
+ foreach (var skill in group.Items.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase))
+ SkillListPanel.Children.Add(CreateSkillCard(skill));
+ }
+ }
+
+ private Border CreateSkillCard(SkillDefinition skill)
+ {
+ var available = skill.IsAvailable;
+ var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue;
+ var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
+ var card = new Border
+ {
+ Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke,
+ BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(10),
+ Padding = new Thickness(12, 10, 12, 10),
+ Margin = new Thickness(0, 0, 0, 6),
+ Opacity = available ? 1.0 : 0.72,
+ };
+ static Brush HexBrush(string hex) => (Brush)new BrushConverter().ConvertFromString(hex)!;
+
+ var root = new StackPanel();
+ var header = new DockPanel();
+ header.Children.Add(new TextBlock
+ {
+ Text = "/" + skill.Name,
+ FontSize = 12.5,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = available ? accentBrush : secondaryText,
+ });
+
+ var badge = new Border
+ {
+ Background = available
+ ? HexBrush("#ECFDF5")
+ : HexBrush("#FEF2F2"),
+ BorderBrush = available
+ ? HexBrush("#BBF7D0")
+ : HexBrush("#FECACA"),
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(999),
+ Padding = new Thickness(7, 2, 7, 2),
+ HorizontalAlignment = HorizontalAlignment.Right,
+ Child = new TextBlock
+ {
+ Text = available ? "사용 가능" : (string.IsNullOrWhiteSpace(skill.UnavailableHint) ? "사용 불가" : skill.UnavailableHint),
+ FontSize = 10,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = available ? HexBrush("#166534") : HexBrush("#991B1B"),
+ }
+ };
+ DockPanel.SetDock(badge, Dock.Right);
+ header.Children.Add(badge);
+ root.Children.Add(header);
+
+ root.Children.Add(new TextBlock
+ {
+ Text = string.IsNullOrWhiteSpace(skill.Label) ? skill.Description : $"{skill.Label} · {skill.Description}",
+ Margin = new Thickness(0, 4, 0, 0),
+ FontSize = 11,
+ TextWrapping = TextWrapping.Wrap,
+ Foreground = secondaryText,
+ });
+
+ if (!string.IsNullOrWhiteSpace(skill.Requires))
+ {
+ root.Children.Add(new TextBlock
+ {
+ Text = $"필요 런타임: {skill.Requires}",
+ Margin = new Thickness(0, 4, 0, 0),
+ FontSize = 10.5,
+ Foreground = secondaryText,
+ });
+ }
+
+ card.Child = root;
+ return card;
+ }
+
+ private void LoadToolCards()
+ {
+ if (_toolCardsLoaded || ToolCardsPanel == null) return;
+ _toolCardsLoaded = true;
+
+ using var tools = ToolRegistry.CreateDefault();
+ var categories = new Dictionary>
+ {
+ ["파일/검색"] = new(),
+ ["문서/리뷰"] = new(),
+ ["코드/개발"] = new(),
+ ["시스템/유틸"] = new(),
+ };
+ var toolCategoryMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["file_read"] = "파일/검색", ["file_write"] = "파일/검색", ["file_edit"] = "파일/검색", ["glob"] = "파일/검색", ["grep"] = "파일/검색",
+ ["document_review"] = "문서/리뷰", ["format_convert"] = "문서/리뷰", ["template_render"] = "문서/리뷰", ["text_summarize"] = "문서/리뷰",
+ ["build_run"] = "코드/개발", ["git_tool"] = "코드/개발", ["lsp"] = "코드/개발", ["code_review"] = "코드/개발", ["test_loop"] = "코드/개발",
+ ["process"] = "시스템/유틸", ["notify"] = "시스템/유틸", ["clipboard"] = "시스템/유틸", ["env"] = "시스템/유틸", ["skill_manager"] = "시스템/유틸",
+ };
+
+ foreach (var tool in tools.All)
+ {
+ var category = toolCategoryMap.TryGetValue(tool.Name, out var mapped) ? mapped : "시스템/유틸";
+ categories[category].Add(tool);
+ }
+
+ foreach (var category in categories)
+ {
+ if (category.Value.Count == 0) continue;
+ ToolCardsPanel.Children.Add(new TextBlock
+ {
+ Text = $"{category.Key} ({category.Value.Count})",
+ FontSize = 12,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
+ Margin = new Thickness(0, 10, 0, 6),
+ });
+
+ var wrap = new WrapPanel();
+ foreach (var tool in category.Value.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase))
+ wrap.Children.Add(CreateToolCard(tool));
+ ToolCardsPanel.Children.Add(wrap);
+ }
+ }
+
+ private Border CreateToolCard(IAgentTool tool)
+ {
+ var enabled = !_disabledTools.Contains(tool.Name);
+ var card = new Border
+ {
+ Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke,
+ BorderBrush = enabled
+ ? (TryFindResource("BorderColor") as Brush ?? Brushes.LightGray)
+ : new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)),
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(10),
+ Padding = new Thickness(10, 8, 10, 8),
+ Margin = new Thickness(0, 0, 8, 8),
+ Width = 232,
+ };
+
+ var grid = new Grid();
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+
+ var info = new StackPanel();
+ info.Children.Add(new TextBlock
+ {
+ Text = tool.Name,
+ FontSize = 12,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
+ });
+ var desc = tool.Description.Length > 56 ? tool.Description[..56] + "…" : tool.Description;
+ info.Children.Add(new TextBlock
+ {
+ Text = desc,
+ FontSize = 10.5,
+ Margin = new Thickness(0, 3, 0, 0),
+ Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
+ TextWrapping = TextWrapping.Wrap,
+ });
+ grid.Children.Add(info);
+
+ var toggle = new CheckBox
+ {
+ IsChecked = enabled,
+ Style = TryFindResource("ToggleSwitch") as Style,
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(8, 0, 0, 0),
+ };
+ toggle.Checked += (_, _) =>
+ {
+ _disabledTools.Remove(tool.Name);
+ card.BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray;
+ };
+ toggle.Unchecked += (_, _) =>
+ {
+ _disabledTools.Add(tool.Name);
+ card.BorderBrush = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26));
+ };
+ Grid.SetColumn(toggle, 1);
+ grid.Children.Add(toggle);
+ card.Child = grid;
+ return card;
+ }
+
+ private void BtnAddHook_Click(object sender, RoutedEventArgs e) => ShowHookEditDialog(null, -1);
+
+ private void ShowHookEditDialog(AgentHookEntry? existing, int index)
+ {
+ var dlg = new Window
+ {
+ Title = existing == null ? "훅 추가" : "훅 편집",
+ Width = 420,
+ SizeToContent = SizeToContent.Height,
+ WindowStartupLocation = WindowStartupLocation.CenterOwner,
+ Owner = this,
+ ResizeMode = ResizeMode.NoResize,
+ WindowStyle = WindowStyle.None,
+ AllowsTransparency = true,
+ Background = Brushes.Transparent,
+ ShowInTaskbar = false,
+ };
+
+ var bgBrush = TryFindResource("LauncherBackground") as Brush ?? Brushes.White;
+ var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
+ var subBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
+ var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke;
+ var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
+
+ var root = new Border
+ {
+ Background = bgBrush,
+ BorderBrush = borderBrush,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(12),
+ Padding = new Thickness(18),
+ };
+ var stack = new StackPanel();
+ root.Child = stack;
+ stack.Children.Add(new TextBlock
+ {
+ Text = existing == null ? "도구 훅 추가" : "도구 훅 편집",
+ FontSize = 15,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = fgBrush,
+ });
+
+ TextBox AddField(string label, string value)
+ {
+ stack.Children.Add(new TextBlock { Text = label, Foreground = subBrush, FontSize = 12, Margin = new Thickness(0, 10, 0, 4) });
+ var box = new TextBox
+ {
+ Text = value,
+ Padding = new Thickness(10, 7, 10, 7),
+ Background = itemBg,
+ BorderBrush = borderBrush,
+ Foreground = fgBrush,
+ FontSize = 12,
+ };
+ stack.Children.Add(box);
+ return box;
+ }
+
+ var nameBox = AddField("이름", existing?.Name ?? "");
+ var toolBox = AddField("대상 도구 (* = 전체)", existing?.ToolName ?? "*");
+ var pathBox = AddField("스크립트 경로", existing?.ScriptPath ?? "");
+ var argsBox = AddField("인수", existing?.Arguments ?? "");
+
+ var timingPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 10, 0, 0) };
+ var pre = new RadioButton { Content = "Pre", Foreground = fgBrush, IsChecked = (existing?.Timing ?? "post") == "pre", Margin = new Thickness(0, 0, 12, 0) };
+ var post = new RadioButton { Content = "Post", Foreground = fgBrush, IsChecked = (existing?.Timing ?? "post") != "pre" };
+ timingPanel.Children.Add(pre);
+ timingPanel.Children.Add(post);
+ stack.Children.Add(timingPanel);
+
+ var actions = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) };
+ var cancel = new Button { Content = "취소", Padding = new Thickness(14, 6, 14, 6), Margin = new Thickness(0, 0, 8, 0) };
+ cancel.Click += (_, _) => dlg.Close();
+ var save = new Button { Content = "저장", Padding = new Thickness(14, 6, 14, 6), IsDefault = true };
+ save.Click += (_, _) =>
+ {
+ var entry = new AgentHookEntry
+ {
+ Name = nameBox.Text.Trim(),
+ ToolName = string.IsNullOrWhiteSpace(toolBox.Text) ? "*" : toolBox.Text.Trim(),
+ Timing = pre.IsChecked == true ? "pre" : "post",
+ ScriptPath = pathBox.Text.Trim(),
+ Arguments = argsBox.Text.Trim(),
+ Enabled = existing?.Enabled ?? true,
+ };
+ if (index >= 0 && index < _llm.AgentHooks.Count)
+ _llm.AgentHooks[index] = entry;
+ else
+ _llm.AgentHooks.Add(entry);
+ BuildHookCards();
+ dlg.Close();
+ };
+ actions.Children.Add(cancel);
+ actions.Children.Add(save);
+ stack.Children.Add(actions);
+
+ dlg.Content = root;
+ dlg.ShowDialog();
+ }
+
+ private void BuildHookCards()
+ {
+ if (HookListPanel == null) return;
+ HookListPanel.Children.Clear();
+
+ for (var i = 0; i < _llm.AgentHooks.Count; i++)
+ {
+ var hook = _llm.AgentHooks[i];
+ var index = i;
+ var card = new Border
+ {
+ Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke,
+ BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(10),
+ Padding = new Thickness(12, 10, 12, 10),
+ Margin = new Thickness(0, 0, 0, 6),
+ };
+ var grid = new Grid();
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+
+ var info = new StackPanel();
+ info.Children.Add(new TextBlock { Text = hook.Name, FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black });
+ info.Children.Add(new TextBlock { Text = $"{hook.Timing?.ToUpperInvariant() ?? "POST"} · {hook.ToolName}", FontSize = 10.5, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, Margin = new Thickness(0, 2, 0, 0) });
+ info.Children.Add(new TextBlock { Text = hook.ScriptPath, FontSize = 10.5, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, Margin = new Thickness(0, 2, 0, 0), TextTrimming = TextTrimming.CharacterEllipsis });
+ grid.Children.Add(info);
+
+ var actionRow = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
+ var toggle = new CheckBox { IsChecked = hook.Enabled, Style = TryFindResource("ToggleSwitch") as Style, Margin = new Thickness(0, 0, 8, 0) };
+ toggle.Checked += (_, _) => hook.Enabled = true;
+ toggle.Unchecked += (_, _) => hook.Enabled = false;
+ var edit = new Button { Content = "편집", Style = TryFindResource("OutlineHoverBtn") as Style, Margin = new Thickness(0, 0, 6, 0) };
+ edit.Click += (_, _) => ShowHookEditDialog(_llm.AgentHooks[index], index);
+ var delete = new Button { Content = "삭제", Style = TryFindResource("OutlineHoverBtn") as Style };
+ delete.Click += (_, _) => { _llm.AgentHooks.RemoveAt(index); BuildHookCards(); };
+ actionRow.Children.Add(toggle);
+ actionRow.Children.Add(edit);
+ actionRow.Children.Add(delete);
+ Grid.SetColumn(actionRow, 1);
+ grid.Children.Add(actionRow);
+
+ card.Child = grid;
+ HookListPanel.Children.Add(card);
+ }
+ }
+
+ private void BtnAddMcpServer_Click(object sender, RoutedEventArgs e)
+ {
+ var nameDialog = new InputDialog("MCP 서버 추가", "서버 이름:", placeholder: "예: filesystem");
+ nameDialog.Owner = this;
+ if (nameDialog.ShowDialog() != true || string.IsNullOrWhiteSpace(nameDialog.ResponseText)) return;
+
+ var commandDialog = new InputDialog("MCP 서버 추가", "실행 명령:", placeholder: "예: npx -y @modelcontextprotocol/server-filesystem");
+ commandDialog.Owner = this;
+ if (commandDialog.ShowDialog() != true || string.IsNullOrWhiteSpace(commandDialog.ResponseText)) return;
+
+ _llm.McpServers.Add(new McpServerEntry
+ {
+ Name = nameDialog.ResponseText.Trim(),
+ Command = commandDialog.ResponseText.Trim(),
+ Enabled = true,
+ });
+ BuildMcpServerCards();
+ }
+
+ private void BuildMcpServerCards()
+ {
+ if (McpServerListPanel == null) return;
+ McpServerListPanel.Children.Clear();
+
+ foreach (var entry in _llm.McpServers.ToList())
+ {
+ var card = new Border
+ {
+ Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke,
+ BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(10),
+ Padding = new Thickness(12, 10, 12, 10),
+ Margin = new Thickness(0, 0, 0, 6),
+ };
+ var grid = new Grid();
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+
+ var info = new StackPanel();
+ info.Children.Add(new TextBlock { Text = entry.Name, FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black });
+ info.Children.Add(new TextBlock { Text = entry.Command, FontSize = 10.5, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, Margin = new Thickness(0, 2, 0, 0), TextTrimming = TextTrimming.CharacterEllipsis });
+ grid.Children.Add(info);
+
+ var actionRow = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
+ var toggle = new CheckBox { IsChecked = entry.Enabled, Style = TryFindResource("ToggleSwitch") as Style, Margin = new Thickness(0, 0, 8, 0) };
+ toggle.Checked += (_, _) => entry.Enabled = true;
+ toggle.Unchecked += (_, _) => entry.Enabled = false;
+ var delete = new Button { Content = "삭제", Style = TryFindResource("OutlineHoverBtn") as Style };
+ delete.Click += (_, _) => { _llm.McpServers.Remove(entry); BuildMcpServerCards(); };
+ actionRow.Children.Add(toggle);
+ actionRow.Children.Add(delete);
+ Grid.SetColumn(actionRow, 1);
+ grid.Children.Add(actionRow);
+
+ card.Child = grid;
+ McpServerListPanel.Children.Add(card);
+ }
+ }
+
+ private void BuildFallbackModelsPanel()
+ {
+ if (FallbackModelsPanel == null) return;
+ FallbackModelsPanel.Children.Clear();
+
+ var sections = new (string Service, string Label, List Models)[]
+ {
+ ("ollama", "Ollama", GetModelCandidates("ollama")),
+ ("vllm", "vLLM", GetModelCandidates("vllm")),
+ ("gemini", "Gemini", new[] { _llm.GeminiModel }.Where(x => !string.IsNullOrWhiteSpace(x)).ToList()!),
+ ("claude", "Claude", new[] { _llm.ClaudeModel }.Where(x => !string.IsNullOrWhiteSpace(x)).ToList()!),
+ };
+
+ foreach (var section in sections)
+ {
+ FallbackModelsPanel.Children.Add(new TextBlock
+ {
+ Text = section.Label,
+ FontSize = 11.5,
+ FontWeight = FontWeights.SemiBold,
+ Margin = new Thickness(0, 8, 0, 4),
+ Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
+ });
+
+ var models = section.Models.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
+ if (models.Count == 0)
+ {
+ FallbackModelsPanel.Children.Add(new TextBlock
+ {
+ Text = "등록된 모델 없음",
+ FontSize = 10.5,
+ Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
+ Margin = new Thickness(8, 0, 0, 4),
+ });
+ continue;
+ }
+
+ foreach (var model in models)
+ {
+ var key = $"{section.Service}:{model}";
+ var row = new Grid { Margin = new Thickness(8, 2, 0, 2) };
+ row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ row.Children.Add(new TextBlock
+ {
+ Text = model,
+ FontSize = 11.5,
+ Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
+ VerticalAlignment = VerticalAlignment.Center,
+ });
+ var toggle = new CheckBox
+ {
+ IsChecked = _llm.FallbackModels.Contains(key, StringComparer.OrdinalIgnoreCase),
+ Style = TryFindResource("ToggleSwitch") as Style,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ toggle.Checked += (_, _) =>
+ {
+ if (!_llm.FallbackModels.Contains(key, StringComparer.OrdinalIgnoreCase))
+ _llm.FallbackModels.Add(key);
+ };
+ toggle.Unchecked += (_, _) => _llm.FallbackModels.RemoveAll(x => x.Equals(key, StringComparison.OrdinalIgnoreCase));
+ Grid.SetColumn(toggle, 1);
+ row.Children.Add(toggle);
+ FallbackModelsPanel.Children.Add(row);
+ }
+ }
+ }
+
private void BtnOpenFullSettings_Click(object sender, RoutedEventArgs e)
{
if (System.Windows.Application.Current is App app)