Files
AX-Copilot/src/AxCopilot/Views/SettingsWindow.xaml.cs

3217 lines
141 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using AxCopilot.Services;
using AxCopilot.ViewModels;
namespace AxCopilot.Views;
public partial class SettingsWindow : Window
{
private readonly SettingsViewModel _vm;
private readonly Action<string> _previewCallback;
private readonly Action _revertCallback;
private bool _saved;
/// <summary>App 인스턴스 캐시 — <c>Application.Current as App</c> 반복 호출 제거.</summary>
private static App? CurrentApp => System.Windows.Application.Current as App;
/// <summary>
/// 핫키 녹화 시작/종료를 외부(App.xaml.cs)에 알리는 콜백.
/// true = 녹화 시작(핫키 일시 정지), false = 녹화 종료(핫키 재개)
/// </summary>
public Action<bool>? SuspendHotkeyCallback { get; set; }
/// <param name="previewCallback">테마 키를 받아 런처에 즉시 미리보기 적용</param>
/// <param name="revertCallback">취소/X 닫기 시 원래 설정으로 복원</param>
public SettingsWindow(SettingsViewModel vm, Action<string> previewCallback, Action revertCallback)
{
InitializeComponent();
_vm = vm;
_previewCallback = previewCallback;
_revertCallback = revertCallback;
DataContext = vm;
vm.ThemePreviewRequested += (_, _) => _previewCallback(vm.SelectedThemeKey);
vm.SaveCompleted += (_, _) =>
{
_saved = true;
Close();
// 인덱스 재빌드를 백그라운드에서 조용히 실행 (UI 차단 없음)
_ = Task.Run(async () =>
{
try
{
var indexService = CurrentApp?.IndexService;
if (indexService != null)
await indexService.BuildAsync();
}
catch (Exception) { /* 인덱싱 실패해도 설정 저장은 완료 */ }
});
};
Loaded += async (_, _) =>
{
RefreshHotkeyBadges();
SetVersionText();
EnsureHotkeyInCombo();
BuildQuoteCategoryCheckboxes();
BuildDockBarSettings();
BuildTextActionCommandsPanel();
MoveBlockSectionToEtc();
// 스킬이 아직 로드되지 않았으면 백그라운드에서 로드 후 UI 구성
var app = CurrentApp;
var skillsLoaded = Services.Agent.SkillService.Skills.Count > 0;
if (!skillsLoaded && (app?.SettingsService?.Settings.Llm.EnableSkillSystem ?? false))
{
// 스킬 로드 (RuntimeDetector 포함)를 백그라운드에서 실행
await Task.Run(() =>
{
Services.Agent.SkillService.EnsureSkillFolder();
Services.Agent.SkillService.LoadSkills(app?.SettingsService?.Settings.Llm.SkillsFolderPath);
});
}
BuildToolRegistryPanel();
LoadAdvancedSettings();
RefreshStorageInfo();
// 개발자 모드는 저장된 설정 유지 (끄면 하위 기능 모두 비활성)
UpdateDevModeContentVisibility();
// AI 기능 토글 초기화
ApplyAiEnabledState(app?.SettingsService?.Settings.AiEnabled ?? false, init: true);
// 네트워크 모드 토글 초기화 (사내 모드 = true = 차단)
if (InternalModeToggle != null)
InternalModeToggle.IsChecked = app?.SettingsService?.Settings.InternalModeEnabled ?? true;
};
}
// ─── 에이전트 차단 섹션 → 기타 탭 이동 ──────────────────────────────────────
private void MoveBlockSectionToEtc()
{
if (AgentBlockSection == null || AgentEtcContent == null) return;
var parent = AgentBlockSection.Parent as Panel;
if (parent != null)
{
parent.Children.Remove(AgentBlockSection);
AgentBlockSection.Margin = new Thickness(0, 0, 0, 16);
AgentEtcContent.Children.Add(AgentBlockSection);
}
}
// ─── 접기/열기 가능 섹션 헬퍼 ──────────────────────────────────────────
private Border CreateCollapsibleSection(string title, UIElement content, bool expanded = true)
{
var headerColor = ThemeResourceHelper.HexBrush("#AAAACC");
var section = new StackPanel();
var contentPanel = new StackPanel { Visibility = expanded ? Visibility.Visible : Visibility.Collapsed };
if (content is Panel panel)
{
// 패널의 자식들을 contentPanel로 옮김 (StackPanel 등)
contentPanel.Children.Add(content);
}
else
{
contentPanel.Children.Add(content);
}
var arrow = new TextBlock
{
Text = "\uE70D",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 9,
Foreground = headerColor,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 6, 0),
RenderTransformOrigin = new Point(0.5, 0.5),
RenderTransform = new RotateTransform(expanded ? 90 : 0),
};
var titleBlock = new TextBlock
{
Text = title,
FontSize = 10.5,
FontWeight = FontWeights.SemiBold,
Foreground = headerColor,
VerticalAlignment = VerticalAlignment.Center,
};
var headerPanel = new StackPanel { Orientation = Orientation.Horizontal };
headerPanel.Children.Add(arrow);
headerPanel.Children.Add(titleBlock);
var header = new Border
{
Background = Brushes.Transparent,
Cursor = Cursors.Hand,
Padding = new Thickness(2, 10, 2, 6),
Child = headerPanel,
};
header.MouseLeftButtonUp += (s, e) =>
{
bool isNowVisible = contentPanel.Visibility == Visibility.Visible;
contentPanel.Visibility = isNowVisible ? Visibility.Collapsed : Visibility.Visible;
arrow.RenderTransform = new RotateTransform(isNowVisible ? 0 : 90);
};
header.MouseEnter += (s, _) => titleBlock.Foreground = ThemeResourceHelper.HexBrush("#CCCCEE");
header.MouseLeave += (s, _) => titleBlock.Foreground = headerColor;
section.Children.Add(header);
section.Children.Add(contentPanel);
return new Border
{
Child = section,
Margin = new Thickness(0, 2, 0, 0),
};
}
private Border CreateCollapsibleSection(string title, IEnumerable<UIElement> children, bool expanded = true)
{
var contentPanel = new StackPanel();
foreach (var child in children)
contentPanel.Children.Add(child);
return CreateCollapsibleSection(title, contentPanel, expanded);
}
// ─── 도구/커넥터 관리 UI (기타 탭) ─────────────────────────────────────────────
private void BuildToolRegistryPanel()
{
if (AgentEtcContent == null) return;
// 도구 목록 데이터 (카테고리별)
var toolGroups = new (string Category, string Icon, string IconColor, (string Name, string Desc)[] Tools)[]
{
("파일/검색", "\uE8B7", "#F59E0B", new[]
{
("file_read", "파일 읽기 — 텍스트/바이너리 파일의 내용을 읽습니다"),
("file_write", "파일 쓰기 — 새 파일을 생성하거나 기존 파일을 덮어씁니다"),
("file_edit", "파일 편집 — 줄 번호 기반으로 파일의 특정 부분을 수정합니다"),
("glob", "파일 패턴 검색 — 글로브 패턴으로 파일을 찾습니다 (예: **/*.cs)"),
("grep_tool", "텍스트 검색 — 정규식으로 파일 내용을 검색합니다"),
("folder_map", "폴더 구조 — 프로젝트의 디렉토리 트리를 조회합니다"),
("document_read", "문서 읽기 — PDF, DOCX 등 문서 파일의 텍스트를 추출합니다"),
}),
("프로세스/빌드", "\uE756", "#06B6D4", new[]
{
("process", "프로세스 실행 — 외부 명령/프로그램을 실행합니다"),
("build_run", "빌드/테스트 — 프로젝트 타입을 감지하여 빌드/테스트를 실행합니다"),
("dev_env_detect", "개발 환경 감지 — IDE, 런타임, 빌드 도구를 자동으로 인식합니다"),
}),
("코드 분석", "\uE943", "#818CF8", new[]
{
("search_codebase", "코드 키워드 검색 — TF-IDF 기반으로 관련 코드를 찾습니다"),
("code_review", "AI 코드 리뷰 — Git diff 분석, 파일 정적 검사, PR 요약"),
("lsp", "LSP 인텔리전스 — 정의 이동, 참조 검색, 심볼 목록 (C#/TS/Python)"),
("test_loop", "테스트 루프 — 테스트 생성/실행/분석 자동화"),
("git_tool", "Git 작업 — status, log, diff, commit 등 Git 명령 실행"),
("snippet_runner", "코드 실행 — C#/Python/JavaScript 스니펫 즉시 실행"),
}),
("문서 생성", "\uE8A5", "#34D399", new[]
{
("excel_create", "Excel 생성 — .xlsx 스프레드시트를 생성합니다"),
("docx_create", "Word 생성 — .docx 문서를 생성합니다"),
("csv_create", "CSV 생성 — CSV 파일을 생성합니다"),
("markdown_create", "마크다운 생성 — .md 파일을 생성합니다"),
("html_create", "HTML 생성 — HTML 파일을 생성합니다"),
("chart_create", "차트 생성 — 데이터 시각화 차트를 생성합니다"),
("batch_create", "배치 생성 — 스크립트 파일을 생성합니다"),
("document_review", "문서 검증 — 문서 품질을 검사합니다"),
("format_convert", "포맷 변환 — 문서 형식을 변환합니다"),
}),
("데이터 처리", "\uE9F5", "#F59E0B", new[]
{
("json_tool", "JSON 처리 — JSON 파싱, 변환, 검증, 포맷팅"),
("regex_tool", "정규식 — 정규식 테스트, 추출, 치환"),
("diff_tool", "텍스트 비교 — 두 파일/텍스트 비교, 통합 diff 출력"),
("base64_tool", "인코딩 — Base64/URL 인코딩, 디코딩"),
("hash_tool", "해시 계산 — MD5/SHA256 해시 계산 (파일/텍스트)"),
("datetime_tool", "날짜/시간 — 날짜 변환, 타임존, 기간 계산"),
}),
("시스템/환경", "\uE770", "#06B6D4", new[]
{
("clipboard_tool", "클립보드 — Windows 클립보드 읽기/쓰기 (텍스트/이미지)"),
("notify_tool", "알림 — Windows 시스템 알림 전송"),
("env_tool", "환경변수 — 환경변수 읽기/설정 (프로세스 범위)"),
("zip_tool", "압축 — 파일 압축(zip) / 해제"),
("http_tool", "HTTP — 로컬/사내 HTTP API 호출 (GET/POST)"),
("sql_tool", "SQLite — SQLite DB 쿼리 실행 (로컬 파일)"),
}),
("에이전트", "\uE99A", "#F472B6", new[]
{
("spawn_agent", "서브에이전트 — 하위 작업을 병렬로 실행하는 서브에이전트를 생성합니다"),
("wait_agents", "에이전트 대기 — 실행 중인 서브에이전트의 결과를 수집합니다"),
("memory", "에이전트 메모리 — 프로젝트 규칙, 선호도를 저장/검색합니다"),
("skill_manager", "스킬 관리 — 스킬 목록 조회, 상세 정보, 재로드"),
("project_rules", "프로젝트 지침 — AX.md 개발 지침을 읽고 씁니다"),
}),
};
// 도구 목록을 섹션으로 그룹화
var toolCards = new List<UIElement>();
// 설명
// 도구 헤더
toolCards.Add(new TextBlock
{
Text = $"등록된 도구/커넥터 ({toolGroups.Sum(g => g.Tools.Length)})",
FontSize = 13,
FontWeight = FontWeights.SemiBold,
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
Margin = new Thickness(2, 4, 0, 4),
});
toolCards.Add(new TextBlock
{
Text = "에이전트가 사용할 수 있는 도구 목록입니다. 코워크/코드 탭에서 LLM이 자동으로 호출합니다.",
FontSize = 11,
Foreground = TryFindResource("SecondaryText") as Brush
?? ThemeResourceHelper.HexBrush("#9999BB"),
Margin = new Thickness(2, 0, 0, 10),
TextWrapping = TextWrapping.Wrap,
});
foreach (var group in toolGroups)
{
// 카테고리 카드
var card = new Border
{
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.White,
CornerRadius = new CornerRadius(10),
Padding = new Thickness(14, 10, 14, 10),
Margin = new Thickness(0, 0, 0, 6),
};
var cardPanel = new StackPanel();
// 도구 아이템 패널 (접기/열기 대상)
var toolItemsPanel = new StackPanel { Visibility = Visibility.Collapsed };
// 접기/열기 화살표
var arrow = new TextBlock
{
Text = "\uE70D",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 9,
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 6, 0),
RenderTransformOrigin = new Point(0.5, 0.5),
RenderTransform = new RotateTransform(0),
};
// 카테고리 헤더 (클릭 가능)
var catHeader = new Border
{
Background = Brushes.Transparent,
Cursor = Cursors.Hand,
};
var catHeaderPanel = new StackPanel { Orientation = Orientation.Horizontal };
catHeaderPanel.Children.Add(arrow);
catHeaderPanel.Children.Add(new TextBlock
{
Text = group.Icon,
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 14,
Foreground = ThemeResourceHelper.HexBrush(group.IconColor),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
});
var catTitle = new TextBlock
{
Text = $"{group.Category} ({group.Tools.Length})",
FontSize = 12.5,
FontWeight = FontWeights.SemiBold,
Foreground = TryFindResource("PrimaryText") as Brush
?? ThemeResourceHelper.HexBrush("#1A1B2E"),
VerticalAlignment = VerticalAlignment.Center,
};
catHeaderPanel.Children.Add(catTitle);
catHeader.Child = catHeaderPanel;
// 클릭 시 접기/열기 토글
catHeader.MouseLeftButtonDown += (s, e) =>
{
e.Handled = true;
bool isVisible = toolItemsPanel.Visibility == Visibility.Visible;
toolItemsPanel.Visibility = isVisible ? Visibility.Collapsed : Visibility.Visible;
arrow.RenderTransform = new RotateTransform(isVisible ? 0 : 90);
};
cardPanel.Children.Add(catHeader);
// 구분선
toolItemsPanel.Children.Add(new Border
{
Height = 1,
Background = TryFindResource("BorderColor") as Brush
?? ThemeResourceHelper.HexBrush("#F0F0F4"),
Margin = new Thickness(0, 6, 0, 4),
});
// 도구 아이템
foreach (var (name, toolDesc) in group.Tools)
{
var row = new Grid { Margin = new Thickness(0, 3, 0, 3) };
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) });
row.ColumnDefinitions.Add(new ColumnDefinition());
var nameBlock = new TextBlock
{
Text = name,
FontSize = 12,
FontFamily = new FontFamily("Consolas, Cascadia Code, Segoe UI"),
Foreground = TryFindResource("AccentColor") as Brush
?? ThemeResourceHelper.HexBrush("#4B5EFC"),
VerticalAlignment = VerticalAlignment.Center,
MinWidth = 130,
Margin = new Thickness(4, 0, 12, 0),
};
Grid.SetColumn(nameBlock, 0);
row.Children.Add(nameBlock);
var descBlock = new TextBlock
{
Text = toolDesc,
FontSize = 11.5,
Foreground = TryFindResource("SecondaryText") as Brush
?? ThemeResourceHelper.HexBrush("#6666AA"),
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.Wrap,
};
Grid.SetColumn(descBlock, 1);
row.Children.Add(descBlock);
toolItemsPanel.Children.Add(row);
}
cardPanel.Children.Add(toolItemsPanel);
card.Child = cardPanel;
toolCards.Add(card);
}
// ── 도구 목록을 직접 추가 (카테고리별 접기/열기) ──
foreach (var card in toolCards)
AgentEtcContent.Children.Insert(AgentEtcContent.Children.Count, card);
int insertIdx = AgentEtcContent.Children.Count;
// ── 로드된 스킬 목록 ──
BuildSkillListSection(ref insertIdx);
}
private void BuildSkillListSection(ref int insertIdx)
{
if (AgentEtcContent == null) return;
var skills = Services.Agent.SkillService.Skills;
if (skills.Count == 0) return;
var accentColor = ThemeResourceHelper.HexColor("#4B5EFC");
var subtleText = ThemeResourceHelper.HexColor("#6666AA");
// 스킬 콘텐츠를 모아서 접기/열기 섹션에 넣기
var skillItems = new List<UIElement>();
// 설명
skillItems.Add(new TextBlock
{
Text = "/ 명령으로 호출할 수 있는 스킬 목록입니다. 앱 내장 + 사용자 추가 스킬이 포함됩니다.\n" +
"(스킬은 사용자가 직접 /명령어를 입력해야 실행됩니다. LLM이 자동 호출하지 않습니다.)",
FontSize = 11,
Foreground = new SolidColorBrush(subtleText),
Margin = new Thickness(2, 0, 0, 10),
TextWrapping = TextWrapping.Wrap,
});
// 내장 스킬 / 고급 스킬 분류
var builtIn = skills.Where(s => string.IsNullOrEmpty(s.Requires)).ToList();
var advanced = skills.Where(s => !string.IsNullOrEmpty(s.Requires)).ToList();
// 내장 스킬 카드
if (builtIn.Count > 0)
{
var card = CreateSkillGroupCard("내장 스킬", "\uE768", "#34D399", builtIn);
skillItems.Add(card);
}
// 고급 스킬 (런타임 의존) 카드
if (advanced.Count > 0)
{
var card = CreateSkillGroupCard("고급 스킬 (런타임 필요)", "\uE9D9", "#A78BFA", advanced);
skillItems.Add(card);
}
// ── 스킬 가져오기/내보내기 버튼 ──
var btnPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(2, 4, 0, 10),
};
// 가져오기 버튼
var importBtn = new Border
{
Background = ThemeResourceHelper.HexBrush("#4B5EFC"),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(14, 7, 14, 7),
Margin = new Thickness(0, 0, 8, 0),
Cursor = Cursors.Hand,
};
var importContent = new StackPanel { Orientation = Orientation.Horizontal };
importContent.Children.Add(new TextBlock
{
Text = "\uE8B5",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 12,
Foreground = Brushes.White,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 6, 0),
});
importContent.Children.Add(new TextBlock
{
Text = "스킬 가져오기 (.zip)",
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = Brushes.White,
VerticalAlignment = VerticalAlignment.Center,
});
importBtn.Child = importContent;
importBtn.MouseLeftButtonUp += SkillImport_Click;
importBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
importBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
btnPanel.Children.Add(importBtn);
// 내보내기 버튼
var exportBtn = new Border
{
Background = ThemeResourceHelper.HexBrush("#F0F1F5"),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(14, 7, 14, 7),
Cursor = Cursors.Hand,
};
var exportContent = new StackPanel { Orientation = Orientation.Horizontal };
exportContent.Children.Add(new TextBlock
{
Text = "\uEDE1",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 12,
Foreground = ThemeResourceHelper.HexBrush("#555"),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 6, 0),
});
exportContent.Children.Add(new TextBlock
{
Text = "스킬 내보내기",
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = ThemeResourceHelper.HexBrush("#444"),
VerticalAlignment = VerticalAlignment.Center,
});
exportBtn.Child = exportContent;
exportBtn.MouseLeftButtonUp += SkillExport_Click;
exportBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
exportBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
btnPanel.Children.Add(exportBtn);
skillItems.Add(btnPanel);
// ── 갤러리/통계 링크 버튼 ──
var linkPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(2, 0, 0, 12),
};
var galleryBtn = new Border
{
CornerRadius = new CornerRadius(8),
Padding = new Thickness(14, 7, 14, 7),
Margin = new Thickness(0, 0, 8, 0),
Background = new SolidColorBrush(Color.FromArgb(0x18, 0x4B, 0x5E, 0xFC)),
Cursor = Cursors.Hand,
};
var galleryContent = new StackPanel { Orientation = Orientation.Horizontal };
galleryContent.Children.Add(new TextBlock
{
Text = "\uE768",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 12,
Foreground = ThemeResourceHelper.HexBrush("#4B5EFC"),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 6, 0),
});
galleryContent.Children.Add(new TextBlock
{
Text = "스킬 갤러리 열기",
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = ThemeResourceHelper.HexBrush("#4B5EFC"),
});
galleryBtn.Child = galleryContent;
galleryBtn.MouseLeftButtonUp += (_, _) =>
{
var win = new SkillGalleryWindow();
win.Owner = Window.GetWindow(this);
win.ShowDialog();
};
galleryBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.8;
galleryBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
linkPanel.Children.Add(galleryBtn);
var statsBtn = new Border
{
CornerRadius = new CornerRadius(8),
Padding = new Thickness(14, 7, 14, 7),
Background = new SolidColorBrush(Color.FromArgb(0x18, 0xA7, 0x8B, 0xFA)),
Cursor = Cursors.Hand,
};
var statsContent = new StackPanel { Orientation = Orientation.Horizontal };
statsContent.Children.Add(new TextBlock
{
Text = "\uE9D9",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 12,
Foreground = ThemeResourceHelper.HexBrush("#A78BFA"),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 6, 0),
});
statsContent.Children.Add(new TextBlock
{
Text = "실행 통계 보기",
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = ThemeResourceHelper.HexBrush("#A78BFA"),
});
statsBtn.Child = statsContent;
statsBtn.MouseLeftButtonUp += (_, _) =>
{
var win = new AgentStatsDashboardWindow();
win.Owner = Window.GetWindow(this);
win.ShowDialog();
};
statsBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.8;
statsBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
linkPanel.Children.Add(statsBtn);
skillItems.Add(linkPanel);
// ── 스킬 섹션 직접 추가 ──
// 스킬 헤더
var skillHeader = new TextBlock
{
Text = $"슬래시 스킬 ({skills.Count})",
FontSize = 13,
FontWeight = FontWeights.SemiBold,
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
Margin = new Thickness(2, 16, 0, 8),
};
AgentEtcContent.Children.Insert(insertIdx++, skillHeader);
foreach (var item in skillItems)
AgentEtcContent.Children.Insert(insertIdx++, item);
}
private Border CreateSkillGroupCard(string title, string icon, string colorHex,
List<Services.Agent.SkillDefinition> skills)
{
var color = ThemeResourceHelper.HexColor(colorHex);
var card = new Border
{
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.White,
CornerRadius = new CornerRadius(10),
Padding = new Thickness(14, 10, 14, 10),
Margin = new Thickness(0, 0, 0, 6),
};
var panel = new StackPanel();
// 스킬 아이템 패널 (접기/열기 대상)
var skillItemsPanel = new StackPanel { Visibility = Visibility.Collapsed };
// 접기/열기 화살표
var arrow = new TextBlock
{
Text = "\uE70D",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 9,
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 6, 0),
RenderTransformOrigin = new Point(0.5, 0.5),
RenderTransform = new RotateTransform(0),
};
// 카테고리 헤더 (클릭 가능)
var catHeader = new Border
{
Background = Brushes.Transparent,
Cursor = Cursors.Hand,
};
var catHeaderPanel = new StackPanel { Orientation = Orientation.Horizontal };
catHeaderPanel.Children.Add(arrow);
catHeaderPanel.Children.Add(new TextBlock
{
Text = icon,
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 14,
Foreground = new SolidColorBrush(color),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
});
catHeaderPanel.Children.Add(new TextBlock
{
Text = $"{title} ({skills.Count})",
FontSize = 12.5,
FontWeight = FontWeights.SemiBold,
Foreground = TryFindResource("PrimaryText") as Brush
?? ThemeResourceHelper.HexBrush("#1A1B2E"),
VerticalAlignment = VerticalAlignment.Center,
});
catHeader.Child = catHeaderPanel;
// 클릭 시 접기/열기 토글
catHeader.MouseLeftButtonDown += (s, e) =>
{
e.Handled = true;
bool isVisible = skillItemsPanel.Visibility == Visibility.Visible;
skillItemsPanel.Visibility = isVisible ? Visibility.Collapsed : Visibility.Visible;
arrow.RenderTransform = new RotateTransform(isVisible ? 0 : 90);
};
panel.Children.Add(catHeader);
// 구분선
skillItemsPanel.Children.Add(new Border
{
Height = 1,
Background = TryFindResource("BorderColor") as Brush
?? ThemeResourceHelper.HexBrush("#F0F0F4"),
Margin = new Thickness(0, 2, 0, 4),
});
// 스킬 아이템
foreach (var skill in skills)
{
var row = new StackPanel { Margin = new Thickness(0, 4, 0, 4) };
// 위 줄: 스킬 명칭 + 가용성 뱃지
var namePanel = new StackPanel { Orientation = Orientation.Horizontal };
namePanel.Children.Add(new TextBlock
{
Text = $"/{skill.Name}",
FontSize = 12,
FontFamily = new FontFamily("Consolas, Cascadia Code, Segoe UI"),
Foreground = skill.IsAvailable
? (TryFindResource("AccentColor") as Brush ?? ThemeResourceHelper.HexBrush("#4B5EFC"))
: Brushes.Gray,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(4, 0, 8, 0),
Opacity = skill.IsAvailable ? 1.0 : 0.5,
});
if (!skill.IsAvailable && !string.IsNullOrEmpty(skill.Requires))
{
namePanel.Children.Add(new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x20, 0xF8, 0x71, 0x71)),
CornerRadius = new CornerRadius(4),
Padding = new Thickness(5, 1, 5, 1),
VerticalAlignment = VerticalAlignment.Center,
Child = new TextBlock
{
Text = skill.UnavailableHint,
FontSize = 9,
Foreground = new SolidColorBrush(Color.FromRgb(0xF8, 0x71, 0x71)),
FontWeight = FontWeights.SemiBold,
},
});
}
else if (skill.IsAvailable && !string.IsNullOrEmpty(skill.Requires))
{
namePanel.Children.Add(new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x20, 0x34, 0xD3, 0x99)),
CornerRadius = new CornerRadius(4),
Padding = new Thickness(5, 1, 5, 1),
VerticalAlignment = VerticalAlignment.Center,
Child = new TextBlock
{
Text = "✓ 사용 가능",
FontSize = 9,
Foreground = new SolidColorBrush(Color.FromRgb(0x34, 0xD3, 0x99)),
FontWeight = FontWeights.SemiBold,
},
});
}
row.Children.Add(namePanel);
// 아래 줄: 설명 (뱃지와 구분되도록 위 여백 추가)
row.Children.Add(new TextBlock
{
Text = $"{skill.Label} — {skill.Description}",
FontSize = 11.5,
Foreground = TryFindResource("SecondaryText") as Brush
?? ThemeResourceHelper.HexBrush("#6666AA"),
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(4, 3, 0, 0),
Opacity = skill.IsAvailable ? 1.0 : 0.5,
});
skillItemsPanel.Children.Add(row);
}
panel.Children.Add(skillItemsPanel);
card.Child = panel;
return card;
}
// ─── 선택 텍스트 AI 명령 설정 ────────────────────────────────────────────────
private void BuildTextActionCommandsPanel()
{
if (TextActionCommandsPanel == null) return;
TextActionCommandsPanel.Children.Clear();
var svc = CurrentApp?.SettingsService;
if (svc == null) return;
var enabled = svc.Settings.Launcher.TextActionCommands;
var toggleStyle = TryFindResource("ToggleSwitch") as Style;
foreach (var (key, label) in TextActionPopup.AvailableCommands)
{
var row = new Grid { Margin = new Thickness(0, 3, 0, 3) };
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var labelBlock = new TextBlock
{
Text = label, FontSize = 12.5,
VerticalAlignment = VerticalAlignment.Center,
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
};
Grid.SetColumn(labelBlock, 0);
row.Children.Add(labelBlock);
var capturedKey = key;
var cb = new CheckBox
{
IsChecked = enabled.Contains(key, StringComparer.OrdinalIgnoreCase),
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
};
if (toggleStyle != null) cb.Style = toggleStyle;
cb.Checked += (_, _) =>
{
if (!enabled.Contains(capturedKey)) enabled.Add(capturedKey);
svc.Save();
};
cb.Unchecked += (_, _) =>
{
// 최소 1개 유지
if (enabled.Count <= 1) { cb.IsChecked = true; return; }
enabled.RemoveAll(x => x == capturedKey);
svc.Save();
};
Grid.SetColumn(cb, 1);
row.Children.Add(cb);
TextActionCommandsPanel.Children.Add(row);
}
}
// ─── 테마 하위 탭 전환 ──────────────────────────────────────────────────────
private void ThemeSubTab_Checked(object sender, RoutedEventArgs e)
{
if (ThemeSelectPanel == null || ThemeColorsPanel == null) return;
if (ThemeSubTabSelect?.IsChecked == true)
{
ThemeSelectPanel.Visibility = Visibility.Visible;
ThemeColorsPanel.Visibility = Visibility.Collapsed;
}
else
{
ThemeSelectPanel.Visibility = Visibility.Collapsed;
ThemeColorsPanel.Visibility = Visibility.Visible;
}
}
// ─── 일반 하위 탭 전환 (일반 + 알림) ──────────────────────────────────────
private bool _notifyMoved;
private void GeneralSubTab_Checked(object sender, RoutedEventArgs e)
{
if (GeneralMainPanel == null || GeneralNotifyPanel == null) return;
// 알림 탭 내용을 최초 1회 NotifyContent로 이동
if (!_notifyMoved && NotifyContent != null)
{
MoveNotifyTabContent();
_notifyMoved = true;
}
GeneralMainPanel.Visibility = GeneralSubTabMain?.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
GeneralNotifyPanel.Visibility = GeneralSubTabNotify?.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
if (GeneralStoragePanel != null)
{
GeneralStoragePanel.Visibility = GeneralSubTabStorage?.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
if (GeneralSubTabStorage?.IsChecked == true) RefreshStorageInfo2();
}
}
private void MoveNotifyTabContent()
{
// XAML의 알림 TabItem에서 ScrollViewer → StackPanel 내용을 NotifyContent로 이동
// 사이드 네비의 "알림" 탭을 찾아서 숨기기
var mainTab = Content as System.Windows.Controls.Grid;
var tabControl = mainTab?.Children.OfType<TabControl>().FirstOrDefault()
?? FindVisualChild<TabControl>(this);
if (tabControl == null) return;
TabItem? notifyTab = null;
foreach (TabItem tab in tabControl.Items)
{
if (tab.Header?.ToString() == "알림") { notifyTab = tab; break; }
}
if (notifyTab?.Content is ScrollViewer sv && sv.Content is StackPanel sp)
{
// 내용물을 NotifyContent로 복제 이동
var children = sp.Children.Cast<UIElement>().ToList();
sp.Children.Clear();
foreach (var child in children)
NotifyContent.Children.Add(child);
}
// 알림 탭 숨기기
if (notifyTab != null) notifyTab.Visibility = Visibility.Collapsed;
}
private static T? FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is T t) return t;
var result = FindVisualChild<T>(child);
if (result != null) return result;
}
return null;
}
// ─── 독 바 설정 ────────────────────────────────────────────────────────────
private void BuildDockBarSettings()
{
var svc = CurrentApp?.SettingsService;
if (svc == null) return;
var launcher = svc.Settings.Launcher;
// 토글 바인딩
ChkDockAutoShow.IsChecked = launcher.DockBarAutoShow;
ChkDockAutoShow.Checked += (_, _) =>
{
launcher.DockBarAutoShow = true; svc.Save();
CurrentApp?.ToggleDockBar();
};
ChkDockAutoShow.Unchecked += (_, _) =>
{
launcher.DockBarAutoShow = false; svc.Save();
// 끄기 시에는 설정만 저장 — 독 바를 토글하지 않음 (이미 표시 중이면 유지, 다음 재시작 시 안 뜸)
};
ChkDockRainbowGlow.IsChecked = launcher.DockBarRainbowGlow;
ChkDockRainbowGlow.Checked += (_, _) => { launcher.DockBarRainbowGlow = true; svc.Save(); RefreshDock(); };
ChkDockRainbowGlow.Unchecked += (_, _) => { launcher.DockBarRainbowGlow = false; svc.Save(); RefreshDock(); };
SliderDockOpacity.Value = launcher.DockBarOpacity;
SliderDockOpacity.ValueChanged += (_, e) => { launcher.DockBarOpacity = e.NewValue; svc.Save(); RefreshDock(); };
// 표시 항목 토글 리스트
if (DockItemsPanel == null) return;
DockItemsPanel.Children.Clear();
var toggleStyle = TryFindResource("ToggleSwitch") as System.Windows.Style;
var enabled = launcher.DockBarItems;
foreach (var (key, icon, tooltip) in DockBarWindow.AvailableItems)
{
var row = new System.Windows.Controls.Grid { Margin = new Thickness(0, 3, 0, 3) };
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var label = new TextBlock
{
Text = $"{tooltip}",
FontSize = 12.5,
VerticalAlignment = VerticalAlignment.Center,
Foreground = TryFindResource("PrimaryText") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Black,
};
System.Windows.Controls.Grid.SetColumn(label, 0);
row.Children.Add(label);
var capturedKey = key;
var cb = new CheckBox
{
IsChecked = enabled.Contains(key),
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
};
if (toggleStyle != null) cb.Style = toggleStyle;
cb.Checked += (_, _) => { if (!enabled.Contains(capturedKey)) enabled.Add(capturedKey); svc.Save(); RefreshDock(); };
cb.Unchecked += (_, _) => { enabled.RemoveAll(x => x == capturedKey); svc.Save(); RefreshDock(); };
System.Windows.Controls.Grid.SetColumn(cb, 1);
row.Children.Add(cb);
DockItemsPanel.Children.Add(row);
}
}
private static SolidColorBrush BrushFromHex(string hex)
{
try { return ThemeResourceHelper.HexBrush(hex); }
catch (Exception) { return new SolidColorBrush(Colors.Gray); }
}
private static void RefreshDock()
{
CurrentApp?.RefreshDockBar();
}
private void BtnDockResetPosition_Click(object sender, RoutedEventArgs e)
{
var result = CustomMessageBox.Show(
"독 바를 화면 하단 중앙으로 이동하시겠습니까?",
"독 바 위치 초기화",
MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes) return;
var svc = CurrentApp?.SettingsService;
if (svc == null) return;
svc.Settings.Launcher.DockBarLeft = -1;
svc.Settings.Launcher.DockBarTop = -1;
svc.Save();
// 즉시 독 바 위치 이동
CurrentApp?.RefreshDockBar();
}
// ─── 저장 공간 관리 ──────────────────────────────────────────────────────
private void RefreshStorageInfo()
{
if (StorageSummaryText == null) return;
var report = StorageAnalyzer.Analyze();
StorageSummaryText.Text = $"앱 전체 사용량: {StorageAnalyzer.FormatSize(report.TotalAppUsage)}";
StorageDriveText.Text = $"드라이브 {report.DriveLabel} 여유: {StorageAnalyzer.FormatSize(report.DriveFreeSpace)} / {StorageAnalyzer.FormatSize(report.DriveTotalSpace)}";
if (StorageDetailPanel == null) return;
StorageDetailPanel.Children.Clear();
var items = new (string Label, long Size)[]
{
("대화 기록", report.Conversations),
("감사 로그", report.AuditLogs),
("앱 로그", report.Logs),
("코드 인덱스", report.CodeIndex),
("임베딩 DB", report.EmbeddingDb),
("클립보드 히스토리", report.ClipboardHistory),
("플러그인", report.Plugins),
("JSON 스킬", report.Skills),
};
foreach (var (label, size) in items)
{
if (size == 0) continue;
var row = new Grid { Margin = new Thickness(0, 2, 0, 2) };
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var labelTb = new TextBlock { Text = label, FontSize = 12, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, VerticalAlignment = VerticalAlignment.Center };
Grid.SetColumn(labelTb, 0);
row.Children.Add(labelTb);
var sizeTb = new TextBlock { Text = StorageAnalyzer.FormatSize(size), FontSize = 12, FontFamily = new FontFamily("Consolas"), Foreground = Brushes.Gray, VerticalAlignment = VerticalAlignment.Center };
Grid.SetColumn(sizeTb, 1);
row.Children.Add(sizeTb);
StorageDetailPanel.Children.Add(row);
}
}
private void BtnStorageRefresh_Click(object sender, RoutedEventArgs e) => RefreshStorageInfo();
private void RefreshStorageInfo2()
{
if (StorageSummaryText2 == null) return;
var report = StorageAnalyzer.Analyze();
StorageSummaryText2.Text = $"앱 전체 사용량: {StorageAnalyzer.FormatSize(report.TotalAppUsage)}";
StorageDriveText2.Text = $"드라이브 {report.DriveLabel} 여유: {StorageAnalyzer.FormatSize(report.DriveFreeSpace)} / {StorageAnalyzer.FormatSize(report.DriveTotalSpace)}";
if (StorageDetailPanel2 == null) return;
StorageDetailPanel2.Children.Clear();
var items = new (string Label, long Size)[]
{
("대화 기록", report.Conversations), ("감사 로그", report.AuditLogs),
("앱 로그", report.Logs), ("코드 인덱스", report.CodeIndex),
("임베딩 DB", report.EmbeddingDb),
("클립보드 히스토리", report.ClipboardHistory),
("플러그인", report.Plugins), ("JSON 스킬", report.Skills),
};
foreach (var (label, size) in items)
{
if (size == 0) continue;
var row = new Grid { Margin = new Thickness(0, 2, 0, 2) };
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var labelTb = new TextBlock { Text = label, FontSize = 12, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black };
Grid.SetColumn(labelTb, 0); row.Children.Add(labelTb);
var sizeTb = new TextBlock { Text = StorageAnalyzer.FormatSize(size), FontSize = 12, FontFamily = new FontFamily("Consolas"), Foreground = Brushes.Gray };
Grid.SetColumn(sizeTb, 1); row.Children.Add(sizeTb);
StorageDetailPanel2.Children.Add(row);
}
}
private void BtnStorageRefresh2_Click(object sender, RoutedEventArgs e) => RefreshStorageInfo2();
private void BtnStorageCleanup_Click(object sender, RoutedEventArgs e)
{
// 테마 리소스 조회
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF));
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x22, 0xFF, 0xFF, 0xFF));
var shadowColor = TryFindResource("ShadowColor") is Color sc ? sc : Colors.Black;
// 보관 기간 선택 팝업 — 커스텀 버튼으로 날짜 선택
var popup = new Window
{
WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent,
Width = 360, SizeToContent = SizeToContent.Height,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = this, ShowInTaskbar = false, Topmost = true,
};
int selectedDays = -1;
var outerBorder = new Border
{
Background = bgBrush, CornerRadius = new CornerRadius(14), BorderBrush = borderBrush,
BorderThickness = new Thickness(1), Margin = new Thickness(16),
Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 20, ShadowDepth = 4, Opacity = 0.3, Color = shadowColor },
};
var stack = new StackPanel { Margin = new Thickness(24, 20, 24, 20) };
stack.Children.Add(new TextBlock { Text = "보관 기간 선택", FontSize = 15, FontWeight = FontWeights.SemiBold, Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 6) });
stack.Children.Add(new TextBlock { Text = "선택한 기간 이전의 데이터를 삭제합니다.\n※ 통계/대화 기록은 삭제되지 않습니다.", FontSize = 12, Foreground = subFgBrush, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 0, 0, 16) });
var btnDays = new (int Days, string Label)[] { (7, "최근 7일만 보관"), (14, "최근 14일만 보관"), (30, "최근 30일만 보관"), (0, "전체 삭제") };
foreach (var (days, label) in btnDays)
{
var d = days;
var isDelete = d == 0;
var deleteBg = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0x44, 0x44));
var deleteBorder = new SolidColorBrush(Color.FromArgb(0x40, 0xFF, 0x44, 0x44));
var deleteText = new SolidColorBrush(Color.FromRgb(0xFF, 0x66, 0x66));
var btn = new Border
{
CornerRadius = new CornerRadius(10), Cursor = Cursors.Hand,
Padding = new Thickness(14, 10, 14, 10), Margin = new Thickness(0, 0, 0, 6),
Background = isDelete ? deleteBg : itemBg,
BorderBrush = isDelete ? deleteBorder : borderBrush,
BorderThickness = new Thickness(1),
};
btn.Child = new TextBlock { Text = label, FontSize = 13, Foreground = isDelete ? deleteText : fgBrush };
var normalBg = isDelete ? deleteBg : itemBg;
btn.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
btn.MouseLeave += (s, _) => { if (s is Border b) b.Background = normalBg; };
btn.MouseLeftButtonUp += (_, _) => { selectedDays = d; popup.Close(); };
stack.Children.Add(btn);
}
// 취소
var cancelBtn = new Border { CornerRadius = new CornerRadius(10), Cursor = Cursors.Hand, Padding = new Thickness(14, 8, 14, 8), Margin = new Thickness(0, 6, 0, 0), Background = Brushes.Transparent };
cancelBtn.Child = new TextBlock { Text = "취소", FontSize = 12, Foreground = subFgBrush, HorizontalAlignment = HorizontalAlignment.Center };
cancelBtn.MouseLeftButtonUp += (_, _) => popup.Close();
stack.Children.Add(cancelBtn);
outerBorder.Child = stack;
popup.Content = outerBorder;
popup.ShowDialog();
if (selectedDays < 0) return;
// 삭제 전 확인
var confirmMsg = selectedDays == 0
? "전체 데이터를 삭제합니다. 정말 진행하시겠습니까?"
: $"최근 {selectedDays}일 이전의 데이터를 삭제합니다. 정말 진행하시겠습니까?";
var confirm = CustomMessageBox.Show(confirmMsg, "삭제 확인", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (confirm != MessageBoxResult.Yes) return;
var freed = StorageAnalyzer.Cleanup(
retainDays: selectedDays,
cleanConversations: false,
cleanAuditLogs: true,
cleanLogs: true,
cleanCodeIndex: true,
cleanClipboard: selectedDays == 0
);
CustomMessageBox.Show(
$"{StorageAnalyzer.FormatSize(freed)}를 확보했습니다.",
"정리 완료", MessageBoxButton.OK, MessageBoxImage.Information);
RefreshStorageInfo();
}
// ─── 알림 카테고리 체크박스 ───────────────────────────────────────────────
private void BuildQuoteCategoryCheckboxes()
{
if (QuoteCategoryPanel == null) return;
QuoteCategoryPanel.Children.Clear();
var enabled = _vm.GetReminderCategories();
var toggleStyle = TryFindResource("ToggleSwitch") as Style;
foreach (var (key, label, countFunc) in Services.QuoteService.Categories)
{
var count = countFunc();
var displayLabel = key == "today_events"
? $"{label} ({count}개, 오늘 {Services.QuoteService.GetTodayMatchCount()}개)"
: $"{label} ({count}개)";
// 좌: 라벨, 우: 토글 스위치
var row = new Grid { Margin = new Thickness(0, 3, 0, 3) };
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var labelBlock = new TextBlock
{
Text = displayLabel,
FontSize = 12.5,
VerticalAlignment = VerticalAlignment.Center,
Foreground = TryFindResource("PrimaryText") as System.Windows.Media.Brush
?? System.Windows.Media.Brushes.White,
};
Grid.SetColumn(labelBlock, 0);
row.Children.Add(labelBlock);
var cb = new CheckBox
{
IsChecked = enabled.Contains(key, StringComparer.OrdinalIgnoreCase),
Tag = key,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
};
if (toggleStyle != null) cb.Style = toggleStyle;
cb.Checked += (s, _) =>
{
if (s is CheckBox c && c.Tag is string k && !enabled.Contains(k))
enabled.Add(k);
};
cb.Unchecked += (s, _) =>
{
if (s is CheckBox c && c.Tag is string k)
enabled.RemoveAll(x => x.Equals(k, StringComparison.OrdinalIgnoreCase));
};
Grid.SetColumn(cb, 1);
row.Children.Add(cb);
QuoteCategoryPanel.Children.Add(row);
}
}
// ─── 버전 표시 ──────────────────────────────────────────────────────────
/// <summary>
/// 하단 버전 텍스트를 AxCopilot.csproj &lt;Version&gt; 값에서 동적으로 읽어 설정합니다.
/// 버전을 올릴 때는 AxCopilot.csproj → &lt;Version&gt; 하나만 수정하면 됩니다.
/// 이 함수와 SettingsWindow.xaml 의 VersionInfoText 는 항상 함께 유지됩니다.
/// </summary>
private void SetVersionText()
{
try
{
var asm = System.Reflection.Assembly.GetExecutingAssembly();
// FileVersionInfo 에서 읽어야 csproj <Version> 이 반영됩니다.
var fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(asm.Location);
var ver = fvi.ProductVersion ?? fvi.FileVersion ?? "?";
// 빌드 메타데이터 제거 (예: "1.0.3+gitabcdef" → "1.0.3")
var plusIdx = ver.IndexOf('+');
if (plusIdx > 0) ver = ver[..plusIdx];
VersionInfoText.Text = $"AX Copilot · v{ver}";
}
catch (Exception)
{
VersionInfoText.Text = "AX Copilot";
}
}
// ─── 핫키 (콤보박스 선택 방식) ──────────────────────────────────────────
/// <summary>이전 녹화기에서 호출되던 초기화 — 콤보박스 전환 후 무연산 (호환용)</summary>
private void RefreshHotkeyBadges() { /* 콤보박스 SelectedValue 바인딩으로 대체 */ }
/// <summary>현재 핫키가 콤보박스 목록에 없으면 항목으로 추가합니다.</summary>
private void EnsureHotkeyInCombo()
{
if (HotkeyCombo == null) return;
var hotkey = _vm.Hotkey;
if (string.IsNullOrWhiteSpace(hotkey)) return;
// 이미 목록에 있는지 확인
foreach (System.Windows.Controls.ComboBoxItem item in HotkeyCombo.Items)
{
if (item.Tag is string tag && tag == hotkey) return;
}
// 목록에 없으면 현재 값을 추가
var display = hotkey.Replace("+", " + ");
var newItem = new System.Windows.Controls.ComboBoxItem
{
Content = $"{display} (사용자 정의)",
Tag = hotkey
};
HotkeyCombo.Items.Insert(0, newItem);
HotkeyCombo.SelectedIndex = 0;
}
/// <summary>Window-level PreviewKeyDown — 핫키 녹화 제거 후 잔여 호출 보호</summary>
private void Window_PreviewKeyDown(object sender, KeyEventArgs e) { }
/// <summary>WPF Key → HotkeyParser가 인식하는 문자열 이름.</summary>
private static string GetKeyName(Key key) => key switch
{
Key.Space => "Space",
Key.Enter or Key.Return => "Enter",
Key.Tab => "Tab",
Key.Back => "Backspace",
Key.Delete => "Delete",
Key.Escape => "Escape",
Key.Home => "Home",
Key.End => "End",
Key.PageUp => "PageUp",
Key.PageDown => "PageDown",
Key.Left => "Left",
Key.Right => "Right",
Key.Up => "Up",
Key.Down => "Down",
Key.Insert => "Insert",
// AZ
>= Key.A and <= Key.Z => key.ToString(),
// 09 (메인 키보드)
>= Key.D0 and <= Key.D9 => ((int)(key - Key.D0)).ToString(),
// F1F12
>= Key.F1 and <= Key.F12 => key.ToString(),
// 기호
Key.OemTilde => "`",
Key.OemMinus => "-",
Key.OemPlus => "=",
Key.OemOpenBrackets => "[",
Key.OemCloseBrackets => "]",
Key.OemPipe or Key.OemBackslash => "\\",
Key.OemSemicolon => ";",
Key.OemQuotes => "'",
Key.OemComma => ",",
Key.OemPeriod => ".",
Key.OemQuestion => "/",
_ => key.ToString()
};
private void HotkeyCombo_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
// 콤보박스 선택이 바뀌면 ViewModel의 Hotkey를 업데이트
// (바인딩이 SelectedValue에 연결되어 자동 처리되지만,
// 기존 RefreshHotkeyBadges 호출은 콤보박스 도입으로 불필요)
}
// ─── 기존 이벤트 핸들러 ──────────────────────────────────────────────────
private async void BtnTestConnection_Click(object sender, RoutedEventArgs e)
{
var btn = sender as Button;
if (btn != null) btn.Content = "테스트 중...";
try
{
// 현재 UI 값으로 임시 LLM 서비스 생성하여 테스트 (설정 저장/창 닫기 없이)
var llm = new Services.LlmService(_vm.Service);
var (ok, msg) = await llm.TestConnectionAsync();
llm.Dispose();
CustomMessageBox.Show(msg, ok ? "연결 성공" : "연결 실패",
MessageBoxButton.OK,
ok ? MessageBoxImage.Information : MessageBoxImage.Warning);
}
catch (Exception ex)
{
CustomMessageBox.Show(ex.Message, "오류", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
if (btn != null) btn.Content = "테스트";
}
}
// ─── 등록 모델 관리 ──────────────────────────────────────────────────
private bool IsEncryptionEnabled =>
CurrentApp?.SettingsService?.Settings.Llm.EncryptionEnabled ?? false;
/// <summary>현재 선택된 서비스 서브탭 이름을 반환합니다.</summary>
private string GetCurrentServiceSubTab()
{
if (SvcTabOllama?.IsChecked == true) return "ollama";
if (SvcTabVllm?.IsChecked == true) return "vllm";
if (SvcTabGemini?.IsChecked == true) return "gemini";
if (SvcTabClaude?.IsChecked == true) return "claude";
return _vm.LlmService; // 폴백
}
private void BtnBrowseSkillFolder_Click(object sender, RoutedEventArgs e)
{
var dlg = new System.Windows.Forms.FolderBrowserDialog
{
Description = "스킬 파일이 있는 폴더를 선택하세요",
ShowNewFolderButton = true,
};
if (!string.IsNullOrEmpty(_vm.SkillsFolderPath) && System.IO.Directory.Exists(_vm.SkillsFolderPath))
dlg.SelectedPath = _vm.SkillsFolderPath;
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
_vm.SkillsFolderPath = dlg.SelectedPath;
}
private void BtnOpenSkillFolder_Click(object sender, RoutedEventArgs e)
{
// 기본 스킬 폴더 열기 (사용자 앱데이터)
var folder = !string.IsNullOrEmpty(_vm.SkillsFolderPath) && System.IO.Directory.Exists(_vm.SkillsFolderPath)
? _vm.SkillsFolderPath
: System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills");
if (!System.IO.Directory.Exists(folder)) System.IO.Directory.CreateDirectory(folder);
try { System.Diagnostics.Process.Start("explorer.exe", folder); } catch (Exception) { }
}
// ─── 스킬 가져오기/내보내기 ──────────────────────────────────────────
private void SkillImport_Click(object sender, MouseButtonEventArgs e)
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
Filter = "스킬 패키지 (*.zip)|*.zip",
Title = "가져올 스킬 zip 파일을 선택하세요",
};
if (dlg.ShowDialog() != true) return;
var count = Services.Agent.SkillService.ImportSkills(dlg.FileName);
if (count > 0)
CustomMessageBox.Show($"스킬 {count}개를 성공적으로 가져왔습니<EC8AB5><EB8B88>.\n스킬 목록이 갱신됩니다.", "스킬 가져오기");
else
CustomMessageBox.Show("스킬 가져오기에 실패했습니다.\nzip 파일에 .skill.md 또는 SKILL.md 파일이 포함되어야 합니다.", "스킬 가져오기");
}
private void SkillExport_Click(object sender, MouseButtonEventArgs e)
{
var skills = Services.Agent.SkillService.Skills;
if (skills.Count == 0)
{
CustomMessageBox.Show("내보낼 스킬이 없습니다.", "스킬 내보내기");
return;
}
// 스킬 선택 팝업
var popup = new Window
{
WindowStyle = WindowStyle.None,
AllowsTransparency = true,
Background = Brushes.Transparent,
SizeToContent = SizeToContent.WidthAndHeight,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = this,
MaxHeight = 450,
};
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 outer = new Border
{
Background = bgBrush,
CornerRadius = new CornerRadius(12),
Padding = new Thickness(20),
MinWidth = 360,
Effect = new System.Windows.Media.Effects.DropShadowEffect
{
BlurRadius = 20, ShadowDepth = 4, Opacity = 0.3,
Color = Colors.Black,
},
};
var mainPanel = new StackPanel();
mainPanel.Children.Add(new TextBlock
{
Text = "내보낼 스킬 선택",
FontSize = 15,
FontWeight = FontWeights.SemiBold,
Foreground = fgBrush,
Margin = new Thickness(0, 0, 0, 12),
});
var checkBoxes = new List<(CheckBox cb, Services.Agent.SkillDefinition skill)>();
var listPanel = new StackPanel();
foreach (var skill in skills)
{
var cb = new CheckBox
{
Content = $"/{skill.Name} — {skill.Label}",
FontSize = 12.5,
Foreground = fgBrush,
Margin = new Thickness(0, 3, 0, 3),
IsChecked = false,
};
checkBoxes.Add((cb, skill));
listPanel.Children.Add(cb);
}
var scrollViewer = new ScrollViewer
{
Content = listPanel,
MaxHeight = 280,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
};
mainPanel.Children.Add(scrollViewer);
// 버튼 행
var btnRow = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Thickness(0, 14, 0, 0),
};
var cancelBtn = new Border
{
Background = ThemeResourceHelper.HexBrush("#F0F1F5"),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(16, 7, 16, 7),
Margin = new Thickness(0, 0, 8, 0),
Cursor = Cursors.Hand,
Child = new TextBlock { Text = "취소", FontSize = 12, Foreground = subFgBrush },
};
cancelBtn.MouseLeftButtonUp += (_, _) => popup.Close();
btnRow.Children.Add(cancelBtn);
var okBtn = new Border
{
Background = ThemeResourceHelper.HexBrush("#4B5EFC"),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(16, 7, 16, 7),
Cursor = Cursors.Hand,
Child = new TextBlock { Text = "내보내기", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White },
};
okBtn.MouseLeftButtonUp += (_, _) =>
{
var selected = checkBoxes.Where(x => x.cb.IsChecked == true).Select(x => x.skill).ToList();
if (selected.Count == 0)
{
CustomMessageBox.Show("내보낼 스킬을 선택하세요.", "스킬 내보내기");
return;
}
// 저장 폴더 선택
var folderDlg = new System.Windows.Forms.FolderBrowserDialog
{
Description = "스킬 zip을 저장할 폴더를 선택하세요",
};
if (folderDlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return;
int exported = 0;
foreach (var skill in selected)
{
var result = Services.Agent.SkillService.ExportSkill(skill, folderDlg.SelectedPath);
if (result != null) exported++;
}
popup.Close();
if (exported > 0)
CustomMessageBox.Show($"{exported}개 스킬을 내보냈습니다.\n경로: {folderDlg.SelectedPath}", "스킬 내보내기");
else
CustomMessageBox.Show("내보내기에 실패했습니다.", "스킬 내보내기");
};
btnRow.Children.Add(okBtn);
mainPanel.Children.Add(btnRow);
outer.Child = mainPanel;
popup.Content = outer;
popup.KeyDown += (_, ke) => { if (ke.Key == Key.Escape) popup.Close(); };
popup.ShowDialog();
}
private void BtnAddModel_Click(object sender, RoutedEventArgs e)
{
var currentService = GetCurrentServiceSubTab();
var dlg = new ModelRegistrationDialog(currentService);
dlg.Owner = this;
if (dlg.ShowDialog() == true)
{
_vm.RegisteredModels.Add(new ViewModels.RegisteredModelRow
{
Alias = dlg.ModelAlias,
EncryptedModelName = Services.CryptoService.EncryptIfEnabled(dlg.ModelName, IsEncryptionEnabled),
Service = currentService,
Endpoint = dlg.Endpoint,
ApiKey = Services.CryptoService.EncryptIfEnabled(dlg.ApiKey, IsEncryptionEnabled),
AuthType = dlg.AuthType,
Cp4dUrl = dlg.Cp4dUrl,
Cp4dUsername = dlg.Cp4dUsername,
Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsEncryptionEnabled),
});
BuildFallbackModelsPanel();
}
}
private void BtnEditModel_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button btn || btn.Tag is not ViewModels.RegisteredModelRow row) return;
string currentModel = Services.CryptoService.DecryptIfEnabled(row.EncryptedModelName, IsEncryptionEnabled);
string currentApiKey = Services.CryptoService.DecryptIfEnabled(row.ApiKey ?? "", IsEncryptionEnabled);
string cp4dPw = Services.CryptoService.DecryptIfEnabled(row.Cp4dPassword ?? "", IsEncryptionEnabled);
var currentService = GetCurrentServiceSubTab();
var dlg = new ModelRegistrationDialog(currentService, row.Alias, currentModel,
row.Endpoint, currentApiKey,
row.AuthType ?? "bearer", row.Cp4dUrl ?? "", row.Cp4dUsername ?? "", cp4dPw);
dlg.Owner = this;
if (dlg.ShowDialog() == true)
{
row.Alias = dlg.ModelAlias;
row.EncryptedModelName = Services.CryptoService.EncryptIfEnabled(dlg.ModelName, IsEncryptionEnabled);
row.Service = currentService;
row.Endpoint = dlg.Endpoint;
row.ApiKey = Services.CryptoService.EncryptIfEnabled(dlg.ApiKey, IsEncryptionEnabled);
row.AuthType = dlg.AuthType;
row.Cp4dUrl = dlg.Cp4dUrl;
row.Cp4dUsername = dlg.Cp4dUsername;
row.Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsEncryptionEnabled);
BuildFallbackModelsPanel();
}
}
private void BtnDeleteModel_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button btn || btn.Tag is not ViewModels.RegisteredModelRow row) return;
var result = CustomMessageBox.Show($"'{row.Alias}' 모델을 삭제하시겠습니까?", "모델 삭제",
MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
_vm.RegisteredModels.Remove(row);
BuildFallbackModelsPanel();
}
}
// ─── 프롬프트 템플릿 관리 ────────────────────────────────────────────
private void BtnAddTemplate_Click(object sender, RoutedEventArgs e)
{
var dlg = new PromptTemplateDialog();
dlg.Owner = this;
if (dlg.ShowDialog() == true)
{
_vm.PromptTemplates.Add(new ViewModels.PromptTemplateRow
{
Name = dlg.TemplateName,
Content = dlg.TemplateContent,
});
}
}
private void BtnEditTemplate_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button btn || btn.Tag is not ViewModels.PromptTemplateRow row) return;
var dlg = new PromptTemplateDialog(row.Name, row.Content);
dlg.Owner = this;
if (dlg.ShowDialog() == true)
{
row.Name = dlg.TemplateName;
row.Content = dlg.TemplateContent;
}
}
private void BtnDeleteTemplate_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button btn || btn.Tag is not ViewModels.PromptTemplateRow row) return;
var result = CustomMessageBox.Show($"'{row.Name}' 템플릿을 삭제하시겠습니까?", "템플릿 삭제",
MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
_vm.PromptTemplates.Remove(row);
}
// ─── AX Agent 서브탭 전환 ───────────────────────────────────────────
private void AgentSubTab_Checked(object sender, RoutedEventArgs e)
{
if (AgentPanelCommon == null) return; // 초기화 전 방어
AgentPanelCommon.Visibility = AgentTabCommon.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
AgentPanelChat.Visibility = AgentTabChat.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
AgentPanelCoworkCode.Visibility = AgentTabCoworkCode.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
AgentPanelCowork.Visibility = AgentTabCowork.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
AgentPanelCode.Visibility = AgentTabCode.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
if (AgentPanelDev != null)
AgentPanelDev.Visibility = AgentTabDev.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
if (AgentPanelEtc != null)
AgentPanelEtc.Visibility = AgentTabEtc.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
if (AgentPanelTools != null)
{
var show = AgentTabTools.IsChecked == true;
AgentPanelTools.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
if (show) LoadToolCards();
}
}
// ─── 도구 관리 카드 UI ──────────────────────────────────────────────
private bool _toolCardsLoaded;
private HashSet<string> _disabledTools = new(StringComparer.OrdinalIgnoreCase);
/// <summary>도구 카드 UI를 카테고리별로 생성합니다.</summary>
private void LoadToolCards()
{
if (_toolCardsLoaded || ToolCardsPanel == null) return;
_toolCardsLoaded = true;
var app = CurrentApp;
var settings = app?.SettingsService?.Settings.Llm;
using var tools = Services.Agent.ToolRegistry.CreateDefault();
_disabledTools = new HashSet<string>(settings?.DisabledTools ?? new(), StringComparer.OrdinalIgnoreCase);
var disabled = _disabledTools;
// 카테고리 매핑
var categories = new Dictionary<string, List<Services.Agent.IAgentTool>>
{
["파일/검색"] = new(),
["문서 생성"] = new(),
["문서 품질"] = new(),
["코드/개발"] = new(),
["데이터/유틸"] = new(),
["시스템"] = new(),
};
var toolCategoryMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
// 파일/검색
["file_read"] = "파일/검색", ["file_write"] = "파일/검색", ["file_edit"] = "파일/검색",
["glob"] = "파일/검색", ["grep"] = "파일/검색", ["folder_map"] = "파일/검색",
["document_read"] = "파일/검색", ["file_watch"] = "파일/검색",
// 문서 생성
["excel_skill"] = "문서 생성", ["docx_skill"] = "문서 생성", ["csv_skill"] = "문서 생성",
["markdown_skill"] = "문서 생성", ["html_skill"] = "문서 생성", ["chart_skill"] = "문서 생성",
["batch_skill"] = "문서 생성", ["pptx_skill"] = "문서 생성",
["document_planner"] = "문서 생성", ["document_assembler"] = "문서 생성",
// 문서 품질
["document_review"] = "문서 품질", ["format_convert"] = "문서 품질",
["template_render"] = "문서 품질", ["text_summarize"] = "문서 품질",
// 코드/개발
["dev_env_detect"] = "코드/개발", ["build_run"] = "코드/개발", ["git_tool"] = "코드/개발",
["lsp"] = "코드/개발", ["sub_agent"] = "코드/개발", ["wait_agents"] = "코드/개발",
["code_search"] = "코드/개발", ["test_loop"] = "코드/개발",
["code_review"] = "코드/개발", ["project_rule"] = "코드/개발",
// 시스템
["process"] = "시스템", ["skill_manager"] = "시스템", ["memory"] = "시스템",
["clipboard"] = "시스템", ["notify"] = "시스템", ["env"] = "시스템",
["image_analyze"] = "시스템",
};
foreach (var tool in tools.All)
{
var cat = toolCategoryMap.TryGetValue(tool.Name, out var c) ? c : "데이터/유틸";
if (categories.ContainsKey(cat))
categories[cat].Add(tool);
else
categories["데이터/유틸"].Add(tool);
}
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0xF5, 0xF5, 0xF8));
foreach (var kv in categories)
{
if (kv.Value.Count == 0) continue;
// 카테고리 헤더
var header = new TextBlock
{
Text = $"{kv.Key} ({kv.Value.Count})",
FontSize = 13, FontWeight = FontWeights.SemiBold,
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
Margin = new Thickness(0, 8, 0, 6),
};
ToolCardsPanel.Children.Add(header);
// 카드 WrapPanel
var wrap = new WrapPanel { Margin = new Thickness(0, 0, 0, 4) };
foreach (var tool in kv.Value.OrderBy(t => t.Name))
{
var isEnabled = !disabled.Contains(tool.Name);
var card = CreateToolCard(tool, isEnabled, disabled, accentBrush, secondaryText, itemBg);
wrap.Children.Add(card);
}
ToolCardsPanel.Children.Add(wrap);
}
// MCP 서버 상태
LoadMcpStatus();
}
/// <summary>개별 도구 카드를 생성합니다 (이름 + 설명 + 토글).</summary>
private Border CreateToolCard(Services.Agent.IAgentTool tool, bool isEnabled,
HashSet<string> disabled, Brush accentBrush, Brush secondaryText, Brush itemBg)
{
var card = new Border
{
Background = itemBg,
CornerRadius = new CornerRadius(8),
Padding = new Thickness(10, 8, 10, 8),
Margin = new Thickness(0, 0, 8, 8),
Width = 240,
BorderBrush = isEnabled ? Brushes.Transparent : new SolidColorBrush(Color.FromArgb(0x30, 0xDC, 0x26, 0x26)),
BorderThickness = new Thickness(1),
};
var grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
// 이름 + 설명
var textStack = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
var nameBlock = new TextBlock
{
Text = tool.Name,
FontSize = 12, FontWeight = FontWeights.SemiBold,
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
TextTrimming = TextTrimming.CharacterEllipsis,
};
textStack.Children.Add(nameBlock);
var desc = tool.Description;
if (desc.Length > 50) desc = desc[..50] + "…";
var descBlock = new TextBlock
{
Text = desc,
FontSize = 10.5,
Foreground = secondaryText,
TextTrimming = TextTrimming.CharacterEllipsis,
Margin = new Thickness(0, 2, 0, 0),
};
textStack.Children.Add(descBlock);
Grid.SetColumn(textStack, 0);
grid.Children.Add(textStack);
// 토글 (CheckBox + ToggleSwitch 스타일)
var toggle = new CheckBox
{
IsChecked = isEnabled,
Style = TryFindResource("ToggleSwitch") as Style,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(8, 0, 0, 0),
};
toggle.Checked += (_, _) =>
{
disabled.Remove(tool.Name);
card.BorderBrush = Brushes.Transparent;
};
toggle.Unchecked += (_, _) =>
{
disabled.Add(tool.Name);
card.BorderBrush = new SolidColorBrush(Color.FromArgb(0x30, 0xDC, 0x26, 0x26));
};
Grid.SetColumn(toggle, 1);
grid.Children.Add(toggle);
card.Child = grid;
return card;
}
/// <summary>MCP 서버 연결 상태 표시를 생성합니다.</summary>
private void LoadMcpStatus()
{
if (McpStatusPanel == null) return;
McpStatusPanel.Children.Clear();
var app = CurrentApp;
var settings = app?.SettingsService?.Settings.Llm;
var mcpServers = settings?.McpServers;
if (mcpServers == null || mcpServers.Count == 0)
{
McpStatusPanel.Children.Add(new TextBlock
{
Text = "등록된 MCP 서버가 없습니다.",
FontSize = 12,
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
Margin = new Thickness(0, 4, 0, 0),
});
return;
}
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0xF5, 0xF5, 0xF8));
foreach (var server in mcpServers)
{
var row = new Border
{
Background = itemBg,
CornerRadius = new CornerRadius(8),
Padding = new Thickness(12, 8, 12, 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 { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
// 상태 아이콘
var statusDot = new Border
{
Width = 8, Height = 8,
CornerRadius = new CornerRadius(4),
Background = new SolidColorBrush(Color.FromRgb(0x34, 0xA8, 0x53)), // 녹색 (등록됨)
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
};
Grid.SetColumn(statusDot, 0);
grid.Children.Add(statusDot);
// 서버 이름 + 명령
var infoStack = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
infoStack.Children.Add(new TextBlock
{
Text = server.Name ?? "(이름 없음)",
FontSize = 12, FontWeight = FontWeights.SemiBold,
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
});
infoStack.Children.Add(new TextBlock
{
Text = server.Command ?? "",
FontSize = 10.5,
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
TextTrimming = TextTrimming.CharacterEllipsis,
});
Grid.SetColumn(infoStack, 1);
grid.Children.Add(infoStack);
// 상태 텍스트
var statusText = new TextBlock
{
Text = "등록됨",
FontSize = 11,
Foreground = new SolidColorBrush(Color.FromRgb(0x34, 0xA8, 0x53)),
VerticalAlignment = VerticalAlignment.Center,
};
Grid.SetColumn(statusText, 2);
grid.Children.Add(statusText);
row.Child = grid;
McpStatusPanel.Children.Add(row);
}
}
// ─── 기능 설정 서브탭 전환 ──────────────────────────────────────────
private void FuncSubTab_Checked(object sender, RoutedEventArgs e)
{
if (FuncPanel_AI == null) return; // 초기화 전 방어
FuncPanel_AI.Visibility = FuncSubTab_AI.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
FuncPanel_Launcher.Visibility = FuncSubTab_Launcher.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
FuncPanel_Design.Visibility = FuncSubTab_Design.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
}
// ─── 접기/열기 섹션 토글 ───────────────────────────────────────────
private void CollapsibleSection_Toggle(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (sender is Border border && border.Tag is System.Windows.Controls.Expander expander)
expander.IsExpanded = !expander.IsExpanded;
}
private void ServiceSubTab_Checked(object sender, RoutedEventArgs e)
{
if (SvcPanelOllama == null) return; // 초기화 전 방어
SvcPanelOllama.Visibility = SvcTabOllama.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
SvcPanelVllm.Visibility = SvcTabVllm.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
SvcPanelGemini.Visibility = SvcTabGemini.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
SvcPanelClaude.Visibility = SvcTabClaude.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
}
private void ThemeCard_Click(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.Tag is string key)
_vm.SelectTheme(key);
}
private void ColorSwatch_Click(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.Tag is ColorRowModel row)
_vm.PickColor(row);
}
private void DevModeCheckBox_Checked(object sender, RoutedEventArgs e)
{
if (sender is not CheckBox cb || !cb.IsChecked.GetValueOrDefault()) return;
// 설정 창 로드 중 바인딩에 의한 자동 Checked 이벤트 무시 (이미 활성화된 상태 복원)
if (!IsLoaded) return;
// 테마 리소스 조회
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E));
var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60));
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40));
// 비밀번호 확인 다이얼로그
var dlg = new Window
{
Title = "개발자 모드 — 비밀번호 확인",
Width = 340, SizeToContent = SizeToContent.Height,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = this, ResizeMode = ResizeMode.NoResize,
WindowStyle = WindowStyle.None, AllowsTransparency = true,
Background = Brushes.Transparent,
};
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 = "\U0001f512 개발자 모드 활성화",
FontSize = 15, FontWeight = FontWeights.SemiBold,
Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 12),
});
stack.Children.Add(new TextBlock
{
Text = "비밀번호를 입력하세요:",
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 == "mouse12#")
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();
if (dlg.ShowDialog() != true)
{
// 비밀번호 실패/취소 — 체크 해제 + DevMode 강제 false
_vm.DevMode = false;
cb.IsChecked = false;
}
UpdateDevModeContentVisibility();
}
private void DevModeCheckBox_Unchecked(object sender, RoutedEventArgs e)
{
UpdateDevModeContentVisibility();
}
/// <summary>개발자 모드 활성화 상태에 따라 개발자 탭 내용 표시/숨김.</summary>
private void UpdateDevModeContentVisibility()
{
if (DevModeContent != null)
DevModeContent.Visibility = _vm.DevMode ? Visibility.Visible : Visibility.Collapsed;
}
// ─── AI 기능 활성화 토글 ────────────────────────────────────────────────
/// <summary>AI 기능 토글 상태를 UI와 설정에 반영합니다.</summary>
private void ApplyAiEnabledState(bool enabled, bool init = false)
{
// 토글 스위치 체크 상태 동기화 (init 시에는 이벤트 억제)
if (AiEnabledToggle != null && AiEnabledToggle.IsChecked != enabled)
{
AiEnabledToggle.IsChecked = enabled;
}
// AX Agent 탭 가시성
if (AgentTabItem != null)
AgentTabItem.Visibility = enabled ? Visibility.Visible : Visibility.Collapsed;
}
private void AiEnabled_Changed(object sender, RoutedEventArgs e)
{
if (!IsLoaded) return;
var tryEnable = AiEnabledToggle?.IsChecked == true;
// 비활성화는 즉시 적용 (비밀번호 불필요)
if (!tryEnable)
{
var app2 = CurrentApp;
if (app2?.SettingsService?.Settings != null)
{
app2.SettingsService.Settings.AiEnabled = false;
app2.SettingsService.Save();
}
ApplyAiEnabledState(false);
return;
}
// 이미 활성화된 상태에서 설정 창이 열릴 때 토글 복원으로 인한 이벤트 → 비밀번호 불필요
var currentApp = CurrentApp;
if (currentApp?.SettingsService?.Settings.AiEnabled == true) return;
// 새로 활성화하는 경우에만 비밀번호 확인
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E));
var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60));
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40));
var dlg = new Window
{
Title = "AI 기능 활성화 — 비밀번호 확인",
Width = 340, SizeToContent = SizeToContent.Height,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = this, ResizeMode = ResizeMode.NoResize,
WindowStyle = WindowStyle.None, AllowsTransparency = true,
Background = Brushes.Transparent,
};
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 = "\U0001f512 AI 기능 활성화",
FontSize = 15, FontWeight = FontWeights.SemiBold,
Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 12),
});
stack.Children.Add(new TextBlock
{
Text = "비밀번호를 입력하세요:",
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 == SettingsPassword)
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();
if (dlg.ShowDialog() == true)
{
var app = CurrentApp;
if (app?.SettingsService?.Settings != null)
{
app.SettingsService.Settings.AiEnabled = true;
app.SettingsService.Save();
}
ApplyAiEnabledState(true);
}
else
{
// 취소/실패 — 토글 원상복구
if (AiEnabledToggle != null) AiEnabledToggle.IsChecked = false;
}
}
// ─── 사내/사외 모드 토글 ─────────────────────────────────────────────────────
private const string SettingsPassword = "axgo123!";
private void NetworkMode_Changed(object sender, RoutedEventArgs e)
{
if (!IsLoaded) return;
var tryInternalMode = InternalModeToggle?.IsChecked == true; // true = 사내(차단), false = 사외(허용)
// 사내 모드로 전환(차단 강화)은 비밀번호 불필요
if (tryInternalMode)
{
var app2 = CurrentApp;
if (app2?.SettingsService?.Settings != null)
{
app2.SettingsService.Settings.InternalModeEnabled = true;
app2.SettingsService.Save();
}
return;
}
// 이미 사외 모드인 경우 토글 복원으로 인한 이벤트 → 비밀번호 불필요
var currentApp = CurrentApp;
if (currentApp?.SettingsService?.Settings.InternalModeEnabled == false) return;
// 사외 모드 활성화(외부 허용)는 비밀번호 확인 필요
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E));
var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60));
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40));
var dlg = new Window
{
Title = "사외 모드 활성화 — 비밀번호 확인",
Width = 340, SizeToContent = SizeToContent.Height,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = this, ResizeMode = ResizeMode.NoResize,
WindowStyle = WindowStyle.None, AllowsTransparency = true,
Background = Brushes.Transparent,
};
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 = "🌐 사외 모드 활성화",
FontSize = 15, FontWeight = FontWeights.SemiBold,
Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 8),
});
stack.Children.Add(new TextBlock
{
Text = "사외 모드에서는 인터넷 검색과 외부 HTTP 접속이 허용됩니다.\n비밀번호를 입력하세요:",
FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 10),
TextWrapping = TextWrapping.Wrap,
});
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 == SettingsPassword)
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();
if (dlg.ShowDialog() == true)
{
var app = CurrentApp;
if (app?.SettingsService?.Settings != null)
{
app.SettingsService.Settings.InternalModeEnabled = false;
app.SettingsService.Save();
}
}
else
{
// 취소/실패 — 토글 원상복구 (사내 모드 유지)
if (InternalModeToggle != null) InternalModeToggle.IsChecked = true;
}
}
private void StepApprovalCheckBox_Checked(object sender, RoutedEventArgs e)
{
if (sender is not CheckBox cb || !cb.IsChecked.GetValueOrDefault()) return;
// 설정 창 로드 중 바인딩에 의한 자동 Checked 이벤트 무시 (이미 활성화된 상태 복원)
if (!IsLoaded) return;
// 테마 리소스 조회
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E));
var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60));
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40));
var dlg = new Window
{
Title = "스텝 바이 스텝 승인 — 비밀번호 확인",
Width = 340, SizeToContent = SizeToContent.Height,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = this, ResizeMode = ResizeMode.NoResize,
WindowStyle = WindowStyle.None, AllowsTransparency = true,
Background = Brushes.Transparent,
};
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 = "\U0001f50d 스텝 바이 스텝 승인 활성화",
FontSize = 15, FontWeight = FontWeights.SemiBold,
Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 12),
});
stack.Children.Add(new TextBlock
{
Text = "개발자 비밀번호를 입력하세요:",
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 == "mouse12#")
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();
if (dlg.ShowDialog() != true)
{
cb.IsChecked = false;
}
}
private void BtnClearMemory_Click(object sender, RoutedEventArgs e)
{
var result = CustomMessageBox.Show(
"에이전트 메모리를 초기화하면 학습된 모든 규칙과 선호도가 삭제됩니다.\n계속하시겠습니까?",
"에이전트 메모리 초기화",
MessageBoxButton.YesNo,
MessageBoxImage.Warning);
if (result != MessageBoxResult.Yes) return;
var app = CurrentApp;
app?.MemoryService?.Clear();
CustomMessageBox.Show("에이전트 메모리가 초기화되었습니다.", "완료", MessageBoxButton.OK, MessageBoxImage.Information);
}
// ─── 에이전트 훅 관리 ─────────────────────────────────────────────────
private void AddHookBtn_Click(object sender, MouseButtonEventArgs e)
{
ShowHookEditDialog(null, -1);
}
/// <summary>플레이스홀더(워터마크) TextBlock 생성 헬퍼.</summary>
private static TextBlock CreatePlaceholder(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 ShowHookEditDialog(Models.AgentHookEntry? existing, int index)
{
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E));
var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60));
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40));
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,
};
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 ? "\u2699 훅 추가" : "\u2699 훅 편집",
FontSize = 15, FontWeight = FontWeights.SemiBold,
Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 14),
});
// ESC 키로 닫기
dlg.KeyDown += (_, e) => { if (e.Key == System.Windows.Input.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 = CreatePlaceholder("예: 코드 리뷰 후 알림", 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 = CreatePlaceholder("예: 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 { Width = new GridLength(1, GridUnitType.Star) });
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 = CreatePlaceholder("예: 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);
Grid.SetColumn(pathInnerGrid, 0);
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,
};
browseBtn.Child = new TextBlock { Text = "...", FontSize = 13, Foreground = accentBrush };
browseBtn.MouseLeftButtonUp += (_, _) =>
{
var ofd = new Microsoft.Win32.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 = CreatePlaceholder("예: --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,
};
cancelBorder.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,
};
saveBorder.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 Models.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 = _vm.Service.Settings.Llm.AgentHooks;
if (isNew)
hooks.Add(entry);
else if (index >= 0 && index < hooks.Count)
hooks[index] = entry;
BuildHookCards();
dlg.Close();
};
btnRow.Children.Add(saveBorder);
stack.Children.Add(btnRow);
border.Child = stack;
dlg.Content = border;
dlg.ShowDialog();
}
private void BuildHookCards()
{
if (HookListPanel == null) return;
HookListPanel.Children.Clear();
var hooks = _vm.Service.Settings.Llm.AgentHooks;
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
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.LightGray,
CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 8, 10, 8),
Margin = new Thickness(0, 0, 0, 4),
};
var grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // 토글
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); // 정보
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,
};
var capturedHook = hook;
toggle.Checked += (_, _) => capturedHook.Enabled = true;
toggle.Unchecked += (_, _) => capturedHook.Enabled = false;
Grid.SetColumn(toggle, 0);
grid.Children.Add(toggle);
// 정보
var info = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
var timingBadge = hook.Timing == "pre" ? "PRE" : "POST";
var timingColor = hook.Timing == "pre" ? "#FF9800" : "#4CAF50";
var headerPanel = new StackPanel { Orientation = Orientation.Horizontal };
headerPanel.Children.Add(new TextBlock
{
Text = hook.Name, FontSize = 13, FontWeight = FontWeights.SemiBold,
Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center,
});
headerPanel.Children.Add(new Border
{
Background = ThemeResourceHelper.HexBrush(timingColor),
CornerRadius = new CornerRadius(4), Padding = new Thickness(5, 1, 5, 1),
Margin = new Thickness(6, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center,
Child = new TextBlock { Text = timingBadge, FontSize = 9, Foreground = Brushes.White, FontWeight = FontWeights.Bold },
});
if (hook.ToolName != "*")
{
headerPanel.Children.Add(new Border
{
Background = new SolidColorBrush(Color.FromArgb(40, 100, 100, 255)),
CornerRadius = new CornerRadius(4), Padding = new Thickness(5, 1, 5, 1),
Margin = new Thickness(4, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center,
Child = new TextBlock { Text = hook.ToolName, FontSize = 9, Foreground = accentBrush },
});
}
info.Children.Add(headerPanel);
info.Children.Add(new TextBlock
{
Text = System.IO.Path.GetFileName(hook.ScriptPath),
FontSize = 11, Foreground = secondaryText,
TextTrimming = TextTrimming.CharacterEllipsis, MaxWidth = 200,
});
Grid.SetColumn(info, 1);
grid.Children.Add(info);
// 편집 버튼
var editBtn = new Border
{
Cursor = Cursors.Hand, VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(4, 0, 4, 0), Padding = new Thickness(6),
};
editBtn.Child = new TextBlock
{
Text = "\uE70F", FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 12, Foreground = secondaryText,
};
editBtn.MouseLeftButtonUp += (_, _) => ShowHookEditDialog(hooks[idx], idx);
Grid.SetColumn(editBtn, 2);
grid.Children.Add(editBtn);
// 삭제 버튼
var delBtn = new Border
{
Cursor = Cursors.Hand, VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 0, 0), Padding = new Thickness(6),
};
delBtn.Child = new TextBlock
{
Text = "\uE74D", FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 12, Foreground = new SolidColorBrush(Color.FromRgb(0xEF, 0x53, 0x50)),
};
delBtn.MouseLeftButtonUp += (_, _) =>
{
hooks.RemoveAt(idx);
BuildHookCards();
};
Grid.SetColumn(delBtn, 3);
grid.Children.Add(delBtn);
card.Child = grid;
HookListPanel.Children.Add(card);
}
}
// ─── MCP 서버 관리 ─────────────────────────────────────────────────
private void BtnAddMcpServer_Click(object sender, RoutedEventArgs e)
{
var dlg = new InputDialog("MCP 서버 추가", "서버 이름:", placeholder: "예: my-mcp-server");
dlg.Owner = this;
if (dlg.ShowDialog() != true || string.IsNullOrWhiteSpace(dlg.ResponseText)) return;
var name = dlg.ResponseText.Trim();
var cmdDlg = new InputDialog("MCP 서버 추가", "실행 명령:", placeholder: "예: npx -y @anthropic/mcp-server");
cmdDlg.Owner = this;
if (cmdDlg.ShowDialog() != true || string.IsNullOrWhiteSpace(cmdDlg.ResponseText)) return;
var entry = new Models.McpServerEntry { Name = name, Command = cmdDlg.ResponseText.Trim(), Enabled = true };
_vm.Service.Settings.Llm.McpServers.Add(entry);
BuildMcpServerCards();
}
private void BuildMcpServerCards()
{
if (McpServerListPanel == null) return;
McpServerListPanel.Children.Clear();
var servers = _vm.Service.Settings.Llm.McpServers;
var primaryText = TryFindResource("PrimaryText") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Black;
var secondaryText = TryFindResource("SecondaryText") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Gray;
var accentBrush = TryFindResource("AccentColor") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Blue;
for (int i = 0; i < servers.Count; i++)
{
var srv = servers[i];
var idx = i;
var card = new Border
{
Background = TryFindResource("ItemBackground") as System.Windows.Media.Brush,
CornerRadius = new CornerRadius(10),
Padding = new Thickness(14, 10, 14, 10),
Margin = new Thickness(0, 4, 0, 0),
BorderBrush = TryFindResource("BorderColor") as System.Windows.Media.Brush,
BorderThickness = new Thickness(1),
};
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 { VerticalAlignment = VerticalAlignment.Center };
info.Children.Add(new TextBlock
{
Text = srv.Name, FontSize = 13.5, FontWeight = FontWeights.SemiBold, Foreground = primaryText,
});
var detailSp = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 0) };
detailSp.Children.Add(new Border
{
Background = srv.Enabled ? accentBrush : System.Windows.Media.Brushes.Gray,
CornerRadius = new CornerRadius(4), Padding = new Thickness(6, 1, 6, 1), Margin = new Thickness(0, 0, 8, 0), Opacity = 0.8,
Child = new TextBlock { Text = srv.Enabled ? "활성" : "비활성", FontSize = 10, Foreground = System.Windows.Media.Brushes.White, FontWeight = FontWeights.SemiBold },
});
detailSp.Children.Add(new TextBlock
{
Text = $"{srv.Command} {string.Join(" ", srv.Args)}", FontSize = 11,
Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center,
MaxWidth = 300, TextTrimming = TextTrimming.CharacterEllipsis,
});
info.Children.Add(detailSp);
Grid.SetColumn(info, 0);
grid.Children.Add(info);
var btnPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
// 활성/비활성 토글
var toggleBtn = new Button
{
Content = srv.Enabled ? "\uE73E" : "\uE711",
FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"),
FontSize = 12, ToolTip = srv.Enabled ? "비활성화" : "활성화",
Background = System.Windows.Media.Brushes.Transparent, BorderThickness = new Thickness(0),
Foreground = srv.Enabled ? accentBrush : System.Windows.Media.Brushes.Gray,
Padding = new Thickness(6, 4, 6, 4), Cursor = Cursors.Hand,
};
toggleBtn.Click += (_, _) => { servers[idx].Enabled = !servers[idx].Enabled; BuildMcpServerCards(); };
btnPanel.Children.Add(toggleBtn);
// 삭제
var delBtn = new Button
{
Content = "\uE74D", FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"),
FontSize = 12, ToolTip = "삭제",
Background = System.Windows.Media.Brushes.Transparent, BorderThickness = new Thickness(0),
Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0xDD, 0x44, 0x44)),
Padding = new Thickness(6, 4, 6, 4), Cursor = Cursors.Hand,
};
delBtn.Click += (_, _) => { servers.RemoveAt(idx); BuildMcpServerCards(); };
btnPanel.Children.Add(delBtn);
Grid.SetColumn(btnPanel, 1);
grid.Children.Add(btnPanel);
card.Child = grid;
McpServerListPanel.Children.Add(card);
}
}
// ─── 감사 로그 폴더 열기 ────────────────────────────────────────────
private void BtnOpenAuditLog_Click(object sender, RoutedEventArgs e)
{
try { System.Diagnostics.Process.Start("explorer.exe", Services.AuditLogService.GetAuditFolder()); } catch (Exception) { }
}
// ─── 폴백/MCP 텍스트 박스 로드/저장 ───────────────────────────────────
private void BuildFallbackModelsPanel()
{
if (FallbackModelsPanel == null) return;
FallbackModelsPanel.Children.Clear();
var llm = _vm.Service.Settings.Llm;
var fallbacks = llm.FallbackModels;
var toggleStyle = TryFindResource("ToggleSwitch") as Style;
// 서비스별로 모델 수집 (순서 고정: Ollama → vLLM → Gemini → Claude)
var sections = new (string Service, string Label, string Color, List<string> Models)[]
{
("ollama", "Ollama", "#107C10", new()),
("vllm", "vLLM", "#0078D4", new()),
("gemini", "Gemini", "#4285F4", new()),
("claude", "Claude", "#8B5CF6", new()),
};
// RegisteredModels → ViewModel과 AppSettings 양쪽에서 수집 (저장 전에도 반영)
// 1) ViewModel의 RegisteredModels (UI에서 방금 추가한 것 포함)
foreach (var row in _vm.RegisteredModels)
{
var svc = (row.Service ?? "").ToLowerInvariant();
var modelName = !string.IsNullOrEmpty(row.Alias) ? row.Alias : row.EncryptedModelName;
var section = sections.FirstOrDefault(s => s.Service == svc);
if (section.Models != null && !string.IsNullOrEmpty(modelName) && !section.Models.Contains(modelName))
section.Models.Add(modelName);
}
// 2) AppSettings의 RegisteredModels (기존 저장된 것 — ViewModel에 없는 경우 보완)
foreach (var m in llm.RegisteredModels)
{
var svc = (m.Service ?? "").ToLowerInvariant();
var modelName = !string.IsNullOrEmpty(m.Alias) ? m.Alias : m.EncryptedModelName;
var section = sections.FirstOrDefault(s => s.Service == svc);
if (section.Models != null && !string.IsNullOrEmpty(modelName) && !section.Models.Contains(modelName))
section.Models.Add(modelName);
}
// 현재 활성 모델 추가 (중복 제거)
if (!string.IsNullOrEmpty(llm.OllamaModel) && !sections[0].Models.Contains(llm.OllamaModel))
sections[0].Models.Add(llm.OllamaModel);
if (!string.IsNullOrEmpty(llm.VllmModel) && !sections[1].Models.Contains(llm.VllmModel))
sections[1].Models.Add(llm.VllmModel);
// Gemini/Claude 고정 모델 목록
foreach (var gm in new[] { "gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash" })
if (!sections[2].Models.Contains(gm)) sections[2].Models.Add(gm);
foreach (var cm in new[] { "claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5", "claude-sonnet-4-5" })
if (!sections[3].Models.Contains(cm)) sections[3].Models.Add(cm);
// 렌더링 — 모델이 없는 섹션도 헤더는 표시
foreach (var (service, svcLabel, svcColor, models) in sections)
{
FallbackModelsPanel.Children.Add(new TextBlock
{
Text = svcLabel,
FontSize = 11, FontWeight = FontWeights.SemiBold,
Foreground = BrushFromHex(svcColor),
Margin = new Thickness(0, 8, 0, 4),
});
if (models.Count == 0)
{
FallbackModelsPanel.Children.Add(new TextBlock
{
Text = "등록된 모델 없음",
FontSize = 11, Foreground = Brushes.Gray, FontStyle = FontStyles.Italic,
Margin = new Thickness(8, 2, 0, 4),
});
continue;
}
foreach (var modelName in models)
{
var fullKey = $"{service}:{modelName}";
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 });
var label = new TextBlock
{
Text = modelName, FontSize = 12, FontFamily = new FontFamily("Consolas, Courier New"),
VerticalAlignment = VerticalAlignment.Center,
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
};
Grid.SetColumn(label, 0);
row.Children.Add(label);
var captured = fullKey;
var cb = new CheckBox
{
IsChecked = fallbacks.Contains(fullKey, StringComparer.OrdinalIgnoreCase),
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
};
if (toggleStyle != null) cb.Style = toggleStyle;
cb.Checked += (_, _) =>
{
if (!fallbacks.Contains(captured)) fallbacks.Add(captured);
FallbackModelsBox.Text = string.Join("\n", fallbacks);
};
cb.Unchecked += (_, _) =>
{
fallbacks.RemoveAll(x => x.Equals(captured, StringComparison.OrdinalIgnoreCase));
FallbackModelsBox.Text = string.Join("\n", fallbacks);
};
Grid.SetColumn(cb, 1);
row.Children.Add(cb);
FallbackModelsPanel.Children.Add(row);
}
}
}
private void LoadAdvancedSettings()
{
var llm = _vm.Service.Settings.Llm;
if (FallbackModelsBox != null)
FallbackModelsBox.Text = string.Join("\n", llm.FallbackModels);
BuildFallbackModelsPanel();
if (McpServersBox != null)
{
try
{
var json = System.Text.Json.JsonSerializer.Serialize(llm.McpServers,
new System.Text.Json.JsonSerializerOptions { WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
McpServersBox.Text = json;
}
catch (Exception) { McpServersBox.Text = "[]"; }
}
BuildMcpServerCards();
BuildHookCards();
}
private void SaveAdvancedSettings()
{
var llm = _vm.Service.Settings.Llm;
if (FallbackModelsBox != null)
{
llm.FallbackModels = FallbackModelsBox.Text
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.Where(s => s.Length > 0)
.ToList();
}
if (McpServersBox != null && !string.IsNullOrWhiteSpace(McpServersBox.Text))
{
try
{
llm.McpServers = System.Text.Json.JsonSerializer.Deserialize<List<Models.McpServerEntry>>(
McpServersBox.Text) ?? new();
}
catch (Exception) { /* JSON 파싱 실패 시 기존 유지 */ }
}
// 도구 비활성 목록 저장
if (_toolCardsLoaded)
llm.DisabledTools = _disabledTools.ToList();
}
private void AddSnippet_Click(object sender, RoutedEventArgs e)
{
if (!_vm.AddSnippet())
CustomMessageBox.Show(
"키워드와 내용은 필수 항목입니다.\n동일한 키워드가 이미 존재하면 추가할 수 없습니다.",
"입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
}
private void DeleteSnippet_Click(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.Tag is ViewModels.SnippetRowModel row)
_vm.RemoveSnippet(row);
}
private void Browse_Click(object sender, RoutedEventArgs e) => _vm.BrowseTarget();
private void AddShortcut_Click(object sender, RoutedEventArgs e)
{
if (!_vm.AddShortcut())
CustomMessageBox.Show(
"키워드와 실행 대상은 필수 항목입니다.\n이미 동일한 키워드가 존재하면 추가할 수 없습니다.",
"입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
}
private void DeleteShortcut_Click(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.Tag is ViewModels.AppShortcutModel shortcut)
_vm.RemoveShortcut(shortcut);
}
private void AddBatchCommand_Click(object sender, RoutedEventArgs e)
{
if (!_vm.AddBatchCommand())
CustomMessageBox.Show(
"키워드와 명령어는 필수 항목입니다.\n동일한 키워드가 이미 존재하면 추가할 수 없습니다.",
"입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
}
private void DeleteBatchCommand_Click(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.Tag is ViewModels.BatchCommandModel cmd)
_vm.RemoveBatchCommand(cmd);
}
private void ResetCommandAliases_Click(object sender, RoutedEventArgs e)
{
_vm.ResetSystemCommandAliases();
}
private void AddIndexPath_Click(object sender, RoutedEventArgs e)
{
var text = NewIndexPathBox?.Text?.Trim() ?? "";
if (!string.IsNullOrWhiteSpace(text))
{
_vm.AddIndexPath(text);
if (NewIndexPathBox != null) NewIndexPathBox.Text = "";
}
}
private void BrowseIndexPath_Click(object sender, RoutedEventArgs e) => _vm.BrowseIndexPath();
private void ResetCapPrefix_Click(object sender, RoutedEventArgs e) => _vm.ResetCapPrefix();
private void ResetCapGlobalHotkey_Click(object sender, RoutedEventArgs e) => _vm.ResetCapGlobalHotkey();
private void CapHotkeyRecorder_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
e.Handled = true;
var key = e.Key == System.Windows.Input.Key.System ? e.SystemKey : e.Key;
// 수정자만 입력된 경우 무시
if (key is System.Windows.Input.Key.LeftCtrl or System.Windows.Input.Key.RightCtrl
or System.Windows.Input.Key.LeftAlt or System.Windows.Input.Key.RightAlt
or System.Windows.Input.Key.LeftShift or System.Windows.Input.Key.RightShift
or System.Windows.Input.Key.LWin or System.Windows.Input.Key.RWin)
return;
if (key == System.Windows.Input.Key.Escape) return;
var parts = new List<string>();
if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Control)) parts.Add("Ctrl");
if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Alt)) parts.Add("Alt");
if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Shift)) parts.Add("Shift");
if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Windows)) parts.Add("Win");
var keyName = key switch
{
System.Windows.Input.Key.Snapshot => "PrintScreen",
System.Windows.Input.Key.Scroll => "ScrollLock",
System.Windows.Input.Key.Pause => "Pause",
_ => key.ToString()
};
parts.Add(keyName);
var hotkey = string.Join("+", parts);
if (AxCopilot.Core.HotkeyParser.TryParse(hotkey, out _))
_vm.CapGlobalHotkey = hotkey;
}
private void ReminderCorner_Click(object sender, RoutedEventArgs e)
{
if (sender is System.Windows.Controls.RadioButton rb && rb.Tag is string corner)
_vm.ReminderCorner = corner;
}
private void ReminderPreview_Click(object sender, RoutedEventArgs e)
{
try
{
var svc = _vm.Service;
var (text, author) = Services.QuoteService.GetRandom(svc.Settings.Reminder.EnabledCategories);
var usage = TimeSpan.FromMinutes(42); // 샘플 값
// 현재 편집 중인 값을 임시로 적용
var cfg = svc.Settings.Reminder;
var origCorner = cfg.Corner;
var origSeconds = cfg.DisplaySeconds;
cfg.Corner = _vm.ReminderCorner;
cfg.DisplaySeconds = _vm.ReminderDisplaySeconds;
var popup = new ReminderPopupWindow(text, author, usage, svc);
popup.Show();
cfg.Corner = origCorner;
cfg.DisplaySeconds = origSeconds;
}
catch (Exception ex)
{
Services.LogService.Warn($"알림 미리보기 실패: {ex.Message}");
}
}
private void RemoveIndexPath_Click(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.Tag is string path)
_vm.RemoveIndexPath(path);
}
private void AddExtension_Click(object sender, RoutedEventArgs e)
{
var text = NewExtensionBox?.Text?.Trim() ?? "";
if (!string.IsNullOrWhiteSpace(text))
{
_vm.AddExtension(text);
if (NewExtensionBox != null) NewExtensionBox.Text = "";
}
}
private void RemoveExtension_Click(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.Tag is string ext)
_vm.RemoveExtension(ext);
}
private void Save_Click(object sender, RoutedEventArgs e)
{
SaveAdvancedSettings(); // 폴백/MCP 텍스트 박스 → settings 저장
_vm.Save();
CustomMessageBox.Show("설정이 저장되었습니다.", "AX Copilot", MessageBoxButton.OK, MessageBoxImage.Information);
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
Close();
}
private void ExportSettings_Click(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.SaveFileDialog
{
Title = "설정 내보내기",
Filter = "AX Copilot 설정|*.axsettings",
FileName = $"AxCopilot_Settings_{DateTime.Now:yyyyMMdd}",
DefaultExt = ".axsettings"
};
if (dlg.ShowDialog() != true) return;
try
{
var srcPath = SettingsService.SettingsPath;
if (System.IO.File.Exists(srcPath))
{
System.IO.File.Copy(srcPath, dlg.FileName, overwrite: true);
CustomMessageBox.Show(
$"설정이 내보내졌습니다:\n{dlg.FileName}",
"AX Copilot", MessageBoxButton.OK,
MessageBoxImage.Information);
}
}
catch (Exception ex)
{
CustomMessageBox.Show($"내보내기 실패: {ex.Message}", "오류");
}
}
private void ImportSettings_Click(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
Title = "설정 불러오기",
Filter = "AX Copilot 설정|*.axsettings|모든 파일|*.*"
};
if (dlg.ShowDialog() != true) return;
var result = CustomMessageBox.Show(
"설정을 불러오면 현재 설정이 덮어씌워집니다.\n계속하시겠습니까?",
"AX Copilot — 설정 불러오기",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result != System.Windows.MessageBoxResult.Yes) return;
try
{
var fileContent = System.IO.File.ReadAllText(dlg.FileName);
// 암호화된 파일인지 판별: PortableDecrypt 시도
var json = CryptoService.PortableDecrypt(fileContent);
if (string.IsNullOrEmpty(json))
{
// 평문 JSON일 수 있음 — 직접 파싱 시도
try
{
var test = System.Text.Json.JsonSerializer.Deserialize<Models.AppSettings>(fileContent);
if (test != null) json = fileContent;
}
catch (Exception) { }
}
if (string.IsNullOrEmpty(json))
{
CustomMessageBox.Show("유효하지 않은 설정 파일입니다.", "오류");
return;
}
// 유효한 설정인지 최종 확인
var settings = System.Text.Json.JsonSerializer.Deserialize<Models.AppSettings>(json);
if (settings == null)
{
CustomMessageBox.Show("설정 파일을 파싱할 수 없습니다.", "오류");
return;
}
// 암호화하여 저장
var encrypted = CryptoService.PortableEncrypt(json);
System.IO.File.WriteAllText(SettingsService.SettingsPath, encrypted);
CustomMessageBox.Show(
"설정이 불러와졌습니다.\n변경 사항을 적용하려면 앱을 재시작하세요.",
"AX Copilot", MessageBoxButton.OK,
MessageBoxImage.Information);
Close();
}
catch (Exception ex)
{
CustomMessageBox.Show($"불러오기 실패: {ex.Message}", "오류");
}
}
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
// 저장하지 않고 닫아도 확인 없이 바로 닫힘 (revert는 OnClosed에서 처리)
base.OnClosing(e);
}
protected override void OnClosed(EventArgs e)
{
if (!_saved) _revertCallback();
base.OnClosed(e);
}
}