Some checks failed
Release Gate / gate (push) Has been cancelled
- SettingsWindow와 ChatWindow에서 숨김 PlanMode row, hidden toggle, dead handler와 binding 제거 - SettingsViewModel dead PlanMode property/save 경로 정리로 현재 정책과 설정 모델을 일치시킴 - README와 DEVELOPMENT 문서에 2026-04-05 21:12 (KST) 기준 변경 내역과 parity 98% 재평가 반영 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
3825 lines
165 KiB
C#
3825 lines
165 KiB
C#
using System.Linq;
|
|
using System.Windows.Data;
|
|
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 const string UnifiedAdminPassword = "axgo123!";
|
|
private readonly SettingsViewModel _vm;
|
|
private readonly Action<string> _previewCallback;
|
|
private readonly Action _revertCallback;
|
|
private bool _saved;
|
|
private bool _isDisplayModeSyncing;
|
|
|
|
/// <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;
|
|
|
|
if (MainSettingsTab != null && AgentTabItem != null && MainSettingsTab.Items.Contains(AgentTabItem))
|
|
MainSettingsTab.Items.Remove(AgentTabItem);
|
|
if (MainSettingsTab != null && AgentShortcutTabItem != null && MainSettingsTab.Items.Contains(AgentShortcutTabItem))
|
|
{
|
|
MainSettingsTab.Items.Remove(AgentShortcutTabItem);
|
|
MainSettingsTab.Items.Add(AgentShortcutTabItem);
|
|
}
|
|
|
|
vm.ThemePreviewRequested += (_, _) => _previewCallback(vm.SelectedThemeKey);
|
|
vm.SaveCompleted += (_, _) =>
|
|
{
|
|
_saved = true;
|
|
Close();
|
|
// 인덱스 재빌드를 백그라운드에서 조용히 실행 (UI 차단 없음)
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
var indexService = ((System.Windows.Application.Current) as App)?.IndexService;
|
|
if (indexService != null)
|
|
await indexService.BuildAsync();
|
|
}
|
|
catch { /* 인덱싱 실패해도 설정 저장은 완료 */ }
|
|
});
|
|
};
|
|
|
|
Loaded += async (_, _) =>
|
|
{
|
|
RefreshHotkeyBadges();
|
|
SetVersionText();
|
|
EnsureHotkeyInCombo();
|
|
BuildQuoteCategoryCheckboxes();
|
|
BuildDockBarSettings();
|
|
BuildTextActionCommandsPanel();
|
|
if (HasLegacyAgentTab())
|
|
{
|
|
MoveBlockSectionToEtc();
|
|
BuildServiceModelPanels();
|
|
}
|
|
|
|
// 스킬이 아직 로드되지 않았으면 백그라운드에서 로드 후 UI 구성
|
|
var app = System.Windows.Application.Current as App;
|
|
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);
|
|
});
|
|
}
|
|
|
|
if (HasLegacyAgentTab())
|
|
{
|
|
BuildToolRegistryPanel();
|
|
LoadAdvancedSettings();
|
|
}
|
|
RefreshStorageInfo();
|
|
// 개발자 모드는 저장된 설정 유지 (끄면 하위 기능 모두 비활성)
|
|
UpdateDevModeContentVisibility();
|
|
// AI 기능 토글 초기화
|
|
ApplyAiEnabledState(app?.SettingsService?.Settings.AiEnabled ?? false, init: true);
|
|
ApplyOperationModeState(app?.SettingsService?.Settings.OperationMode);
|
|
InitializeDisplayModeUi();
|
|
if (HasLegacyAgentTab())
|
|
SyncAgentSelectionCards();
|
|
};
|
|
}
|
|
|
|
private bool HasLegacyAgentTab()
|
|
=> MainSettingsTab != null && AgentTabItem != null && MainSettingsTab.Items.Contains(AgentTabItem);
|
|
|
|
private void SyncAgentSelectionCards()
|
|
{
|
|
var service = (_vm.LlmService ?? "").Trim().ToLowerInvariant();
|
|
if (AgentServiceCardOllama != null) AgentServiceCardOllama.IsChecked = service == "ollama";
|
|
if (AgentServiceCardVllm != null) AgentServiceCardVllm.IsChecked = service == "vllm";
|
|
if (AgentServiceCardGemini != null) AgentServiceCardGemini.IsChecked = service == "gemini";
|
|
if (AgentServiceCardClaude != null) AgentServiceCardClaude.IsChecked = service == "claude";
|
|
|
|
var permission = Services.Agent.PermissionModeCatalog.NormalizeGlobalMode(_vm.DefaultAgentPermission);
|
|
if (AgentPermissionCardAsk != null) AgentPermissionCardAsk.IsChecked = permission == "Ask";
|
|
if (AgentPermissionCardPlan != null) AgentPermissionCardPlan.IsChecked = permission == "Plan";
|
|
if (AgentPermissionCardAuto != null) AgentPermissionCardAuto.IsChecked = permission == "Auto";
|
|
if (AgentPermissionCardDeny != null) AgentPermissionCardDeny.IsChecked = permission == "Deny";
|
|
|
|
var decision = (_vm.AgentDecisionLevel ?? "normal").Trim().ToLowerInvariant();
|
|
if (AgentDecisionCardMinimal != null) AgentDecisionCardMinimal.IsChecked = decision == "minimal";
|
|
if (AgentDecisionCardNormal != null) AgentDecisionCardNormal.IsChecked = decision == "normal";
|
|
if (AgentDecisionCardDetailed != null) AgentDecisionCardDetailed.IsChecked = decision == "detailed";
|
|
|
|
var operationMode = OperationModePolicy.Normalize(_vm.OperationMode);
|
|
if (AgentOperationModeInternal != null) AgentOperationModeInternal.IsChecked = operationMode == OperationModePolicy.InternalMode;
|
|
if (AgentOperationModeExternal != null) AgentOperationModeExternal.IsChecked = operationMode == OperationModePolicy.ExternalMode;
|
|
|
|
var maxContextTokens = _vm.LlmMaxContextTokens;
|
|
if (AgentContextTokens4K != null) AgentContextTokens4K.IsChecked = maxContextTokens <= 4096;
|
|
if (AgentContextTokens16K != null) AgentContextTokens16K.IsChecked = maxContextTokens > 4096 && maxContextTokens <= 16384;
|
|
if (AgentContextTokens64K != null) AgentContextTokens64K.IsChecked = maxContextTokens > 16384 && maxContextTokens <= 65536;
|
|
if (AgentContextTokens256K != null) AgentContextTokens256K.IsChecked = maxContextTokens > 65536 && maxContextTokens <= 262144;
|
|
if (AgentContextTokens1M != null) AgentContextTokens1M.IsChecked = maxContextTokens > 262144;
|
|
|
|
var retentionDays = _vm.LlmRetentionDays;
|
|
if (AgentRetentionDays7 != null) AgentRetentionDays7.IsChecked = retentionDays == 7;
|
|
if (AgentRetentionDays30 != null) AgentRetentionDays30.IsChecked = retentionDays == 30;
|
|
if (AgentRetentionDays90 != null) AgentRetentionDays90.IsChecked = retentionDays == 90;
|
|
if (AgentRetentionDaysUnlimited != null) AgentRetentionDaysUnlimited.IsChecked = retentionDays == 0;
|
|
|
|
var logLevel = (_vm.AgentLogLevel ?? "simple").Trim().ToLowerInvariant();
|
|
if (AgentLogLevelSimple != null) AgentLogLevelSimple.IsChecked = logLevel == "simple";
|
|
if (AgentLogLevelDetailed != null) AgentLogLevelDetailed.IsChecked = logLevel == "detailed";
|
|
if (AgentLogLevelDebug != null) AgentLogLevelDebug.IsChecked = logLevel == "debug";
|
|
}
|
|
|
|
private void InitializeDisplayModeUi()
|
|
{
|
|
var app = System.Windows.Application.Current as App;
|
|
if (app?.SettingsService?.Settings?.Llm != null)
|
|
app.SettingsService.Settings.Llm.AgentUiExpressionLevel = "rich";
|
|
SetDisplayMode("rich", persist: false);
|
|
}
|
|
|
|
private void DisplayMode_Checked(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isDisplayModeSyncing) return;
|
|
var rb = sender as RadioButton;
|
|
var next = rb?.Name switch
|
|
{
|
|
"DisplayModeRich" => "rich",
|
|
"DisplayModeSimple" => "simple",
|
|
_ => "balanced",
|
|
};
|
|
SetDisplayMode(next, persist: true);
|
|
}
|
|
|
|
private void AgentDisplayMode_Checked(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isDisplayModeSyncing) return;
|
|
SetDisplayMode("rich", persist: true);
|
|
}
|
|
|
|
private void SetDisplayMode(string mode, bool persist)
|
|
{
|
|
mode = NormalizeDisplayMode(mode);
|
|
|
|
_isDisplayModeSyncing = true;
|
|
try
|
|
{
|
|
var rich = GetDisplayModeRadio("DisplayModeRich");
|
|
var balanced = GetDisplayModeRadio("DisplayModeBalanced");
|
|
var simple = GetDisplayModeRadio("DisplayModeSimple");
|
|
var agentRich = GetDisplayModeRadio("AgentDisplayModeRich");
|
|
var agentBalanced = GetDisplayModeRadio("AgentDisplayModeBalanced");
|
|
var agentSimple = GetDisplayModeRadio("AgentDisplayModeSimple");
|
|
if (rich != null) rich.IsChecked = mode == "rich";
|
|
if (balanced != null) balanced.IsChecked = mode == "balanced";
|
|
if (simple != null) simple.IsChecked = mode == "simple";
|
|
if (agentRich != null) agentRich.IsChecked = mode == "rich";
|
|
if (agentBalanced != null) agentBalanced.IsChecked = mode == "balanced";
|
|
if (agentSimple != null) agentSimple.IsChecked = mode == "simple";
|
|
}
|
|
finally
|
|
{
|
|
_isDisplayModeSyncing = false;
|
|
}
|
|
|
|
ApplyMainTabVisibility(mode);
|
|
if (HasLegacyAgentTab())
|
|
ApplyAgentSubTabVisibility(mode);
|
|
|
|
if (!persist) return;
|
|
var app = System.Windows.Application.Current as App;
|
|
if (app?.SettingsService?.Settings?.Llm == null) return;
|
|
app.SettingsService.Settings.Llm.AgentUiExpressionLevel = mode;
|
|
app.SettingsService.Save();
|
|
}
|
|
|
|
private void ApplyMainTabVisibility(string mode)
|
|
{
|
|
if (MainSettingsTab == null) return;
|
|
|
|
var simpleKeep = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"일반", "테마", "기능"
|
|
};
|
|
var balancedKeep = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"일반", "테마", "클립보드", "캡처", "시스템", "기능"
|
|
};
|
|
|
|
foreach (var item in MainSettingsTab.Items.OfType<TabItem>())
|
|
{
|
|
var header = item.Header?.ToString() ?? "";
|
|
var visible = mode switch
|
|
{
|
|
"simple" => simpleKeep.Contains(header),
|
|
"balanced" => balancedKeep.Contains(header),
|
|
_ => true,
|
|
};
|
|
|
|
if (string.Equals(header, "알림", StringComparison.OrdinalIgnoreCase))
|
|
visible = false;
|
|
item.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
|
|
}
|
|
|
|
if (MainSettingsTab.SelectedItem is TabItem selected && selected.Visibility == Visibility.Visible)
|
|
return;
|
|
|
|
var firstVisible = MainSettingsTab.Items.OfType<TabItem>().FirstOrDefault(t => t.Visibility == Visibility.Visible);
|
|
if (firstVisible != null)
|
|
MainSettingsTab.SelectedItem = firstVisible;
|
|
}
|
|
|
|
private void ApplyAgentSubTabVisibility(string mode)
|
|
{
|
|
if (AgentTabCommon == null) return;
|
|
|
|
SetSubTabVisible(AgentTabCommon, true);
|
|
SetSubTabVisible(AgentTabChat, true);
|
|
SetSubTabVisible(AgentTabCoworkCode, true);
|
|
SetSubTabVisible(AgentTabDev, true);
|
|
SetSubTabVisible(AgentTabCowork, false);
|
|
SetSubTabVisible(AgentTabCode, false);
|
|
SetSubTabVisible(AgentTabTools, false);
|
|
SetSubTabVisible(AgentTabEtc, false);
|
|
|
|
if (AgentTabCommon.IsChecked != true
|
|
&& AgentTabChat.IsChecked != true
|
|
&& AgentTabCoworkCode.IsChecked != true
|
|
&& AgentTabDev.IsChecked != true)
|
|
{
|
|
AgentTabCommon.IsChecked = true;
|
|
}
|
|
|
|
AgentSubTab_Checked(this, new RoutedEventArgs());
|
|
}
|
|
|
|
private static void SetSubTabVisible(RadioButton? tab, bool visible)
|
|
{
|
|
if (tab == null) return;
|
|
tab.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
|
|
if (!visible) tab.IsChecked = false;
|
|
}
|
|
|
|
private static string NormalizeDisplayMode(string? mode)
|
|
{
|
|
return (mode ?? "balanced").Trim().ToLowerInvariant() switch
|
|
{
|
|
"rich" => "rich",
|
|
"simple" => "simple",
|
|
_ => "balanced",
|
|
};
|
|
}
|
|
|
|
private RadioButton? GetDisplayModeRadio(string name)
|
|
{
|
|
if (MainSettingsTab?.Template == null) return null;
|
|
return MainSettingsTab.Template.FindName(name, MainSettingsTab) as RadioButton;
|
|
}
|
|
|
|
// ─── 에이전트 차단 섹션 → 기타 탭 이동 ──────────────────────────────────────
|
|
|
|
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 = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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 = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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", "프로젝트 지침 ? AGENTS.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
|
|
?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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 = new SolidColorBrush((Color)ColorConverter.ConvertFromString(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
|
|
?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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
|
|
?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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
|
|
?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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
|
|
?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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 = (Color)ColorConverter.ConvertFromString("#4B5EFC");
|
|
var subtleText = (Color)ColorConverter.ConvertFromString("#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 = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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 = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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 = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#555")),
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(0, 0, 6, 0),
|
|
});
|
|
exportContent.Children.Add(new TextBlock
|
|
{
|
|
Text = "스킬 내보내기",
|
|
FontSize = 12,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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 = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4B5EFC")),
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(0, 0, 6, 0),
|
|
});
|
|
galleryContent.Children.Add(new TextBlock
|
|
{
|
|
Text = "스킬 갤러리 열기",
|
|
FontSize = 12,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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 = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#A78BFA")),
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(0, 0, 6, 0),
|
|
});
|
|
statsContent.Children.Add(new TextBlock
|
|
{
|
|
Text = "실행 통계 보기",
|
|
FontSize = 12,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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 = (Color)ColorConverter.ConvertFromString(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
|
|
?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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
|
|
?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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 ?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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
|
|
?? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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 = (System.Windows.Application.Current as App)?.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 = (System.Windows.Application.Current as App)?.SettingsService;
|
|
if (svc == null) return;
|
|
var launcher = svc.Settings.Launcher;
|
|
|
|
// 토글 바인딩
|
|
ChkDockAutoShow.IsChecked = launcher.DockBarAutoShow;
|
|
ChkDockAutoShow.Checked += (_, _) =>
|
|
{
|
|
launcher.DockBarAutoShow = true; svc.Save();
|
|
(System.Windows.Application.Current as App)?.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 new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex)); }
|
|
catch { return new SolidColorBrush(Colors.Gray); }
|
|
}
|
|
|
|
private static void RefreshDock()
|
|
{
|
|
(System.Windows.Application.Current as App)?.RefreshDockBar();
|
|
}
|
|
|
|
private void BtnDockResetPosition_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var result = CustomMessageBox.Show(
|
|
"독 바를 화면 하단 중앙으로 이동하시겠습니까?",
|
|
"독 바 위치 초기화",
|
|
MessageBoxButton.YesNo, MessageBoxImage.Question);
|
|
if (result != MessageBoxResult.Yes) return;
|
|
|
|
var svc = (System.Windows.Application.Current as App)?.SettingsService;
|
|
if (svc == null) return;
|
|
svc.Settings.Launcher.DockBarLeft = -1;
|
|
svc.Settings.Launcher.DockBarTop = -1;
|
|
svc.Save();
|
|
|
|
// 즉시 독 바 위치 이동
|
|
(System.Windows.Application.Current as App)?.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 <Version> 값에서 동적으로 읽어 설정합니다.
|
|
/// 버전을 올릴 때는 AxCopilot.csproj → <Version> 하나만 수정하면 됩니다.
|
|
/// 이 함수와 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
|
|
{
|
|
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",
|
|
// A?Z
|
|
>= Key.A and <= Key.Z => key.ToString(),
|
|
// 0?9 (메인 키보드)
|
|
>= Key.D0 and <= Key.D9 => ((int)(key - Key.D0)).ToString(),
|
|
// F1?F12
|
|
>= 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 =>
|
|
(Application.Current as App)?.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 (SvcTabSigmoid?.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 { }
|
|
}
|
|
|
|
// ─── 스킬 가져오기/내보내기 ──────────────────────────────────────────
|
|
|
|
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}개를 성공적으로 가져왔습니??.\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 = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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 = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#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 = dlg.ApiKey,
|
|
AllowInsecureTls = dlg.AllowInsecureTls,
|
|
AuthType = dlg.AuthType,
|
|
Cp4dUrl = dlg.Cp4dUrl,
|
|
Cp4dUsername = dlg.Cp4dUsername,
|
|
Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsEncryptionEnabled),
|
|
});
|
|
BuildFallbackModelsPanel();
|
|
BuildServiceModelPanels();
|
|
}
|
|
}
|
|
|
|
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 cp4dPw = Services.CryptoService.DecryptIfEnabled(row.Cp4dPassword ?? "", IsEncryptionEnabled);
|
|
|
|
var currentService = GetCurrentServiceSubTab();
|
|
var dlg = new ModelRegistrationDialog(currentService, row.Alias, currentModel,
|
|
row.Endpoint, row.ApiKey, row.AllowInsecureTls,
|
|
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 = dlg.ApiKey;
|
|
row.AllowInsecureTls = dlg.AllowInsecureTls;
|
|
row.AuthType = dlg.AuthType;
|
|
row.Cp4dUrl = dlg.Cp4dUrl;
|
|
row.Cp4dUsername = dlg.Cp4dUsername;
|
|
row.Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsEncryptionEnabled);
|
|
BuildFallbackModelsPanel();
|
|
BuildServiceModelPanels();
|
|
}
|
|
}
|
|
|
|
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();
|
|
BuildServiceModelPanels();
|
|
}
|
|
}
|
|
|
|
// ─── 프롬프트 템플릿 관리 ────────────────────────────────────────────
|
|
|
|
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; // 초기화 전 방어
|
|
var showCommon = AgentTabCommon.IsChecked == true;
|
|
var showService = AgentTabChat.IsChecked == true;
|
|
var showPermission = AgentTabCoworkCode.IsChecked == true;
|
|
var showAdvanced = AgentTabDev.IsChecked == true;
|
|
|
|
AgentPanelCommon.Visibility = showCommon || showService ? Visibility.Visible : Visibility.Collapsed;
|
|
if (AgentCommonOverviewSection != null)
|
|
AgentCommonOverviewSection.Visibility = showCommon ? Visibility.Visible : Visibility.Collapsed;
|
|
if (AgentCommonRuntimeSection != null)
|
|
AgentCommonRuntimeSection.Visibility = showCommon ? Visibility.Visible : Visibility.Collapsed;
|
|
if (AgentServiceSection != null)
|
|
AgentServiceSection.Visibility = showService ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
AgentPanelCoworkCode.Visibility = showPermission ? Visibility.Visible : Visibility.Collapsed;
|
|
AgentPanelChat.Visibility = Visibility.Collapsed;
|
|
AgentPanelCowork.Visibility = Visibility.Collapsed;
|
|
AgentPanelCode.Visibility = Visibility.Collapsed;
|
|
|
|
if (AgentPanelDev != null)
|
|
AgentPanelDev.Visibility = showAdvanced ? Visibility.Visible : Visibility.Collapsed;
|
|
if (AgentPanelEtc != null)
|
|
AgentPanelEtc.Visibility = Visibility.Collapsed;
|
|
if (AgentPanelTools != null)
|
|
AgentPanelTools.Visibility = Visibility.Collapsed;
|
|
}
|
|
|
|
// ─── 도구 관리 카드 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 = System.Windows.Application.Current as App;
|
|
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 = System.Windows.Application.Current as App;
|
|
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;
|
|
SvcPanelSigmoid.Visibility = SvcTabSigmoid.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
|
BuildServiceModelPanels();
|
|
SyncAgentSelectionCards();
|
|
}
|
|
|
|
private void AgentServiceCard_Checked(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true) return;
|
|
var service = rb.Name switch
|
|
{
|
|
"AgentServiceCardVllm" => "vllm",
|
|
"AgentServiceCardGemini" => "gemini",
|
|
"AgentServiceCardClaude" => "claude",
|
|
_ => "ollama",
|
|
};
|
|
|
|
_vm.LlmService = service;
|
|
if (SvcTabOllama != null) SvcTabOllama.IsChecked = service == "ollama";
|
|
if (SvcTabVllm != null) SvcTabVllm.IsChecked = service == "vllm";
|
|
if (SvcTabGemini != null) SvcTabGemini.IsChecked = service == "gemini";
|
|
if (SvcTabSigmoid != null) SvcTabSigmoid.IsChecked = service == "claude";
|
|
BuildServiceModelPanels();
|
|
}
|
|
|
|
private void AgentPermissionCard_Checked(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true) return;
|
|
_vm.DefaultAgentPermission = rb.Name switch
|
|
{
|
|
"AgentPermissionCardPlan" => "Plan",
|
|
"AgentPermissionCardAuto" => "Auto",
|
|
"AgentPermissionCardDeny" => "Deny",
|
|
_ => "Ask",
|
|
};
|
|
}
|
|
|
|
private void AgentDecisionCard_Checked(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true) return;
|
|
_vm.AgentDecisionLevel = rb.Name switch
|
|
{
|
|
"AgentDecisionCardMinimal" => "minimal",
|
|
"AgentDecisionCardDetailed" => "detailed",
|
|
_ => "normal",
|
|
};
|
|
}
|
|
|
|
private void AgentOperationModeCard_Checked(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true)
|
|
return;
|
|
|
|
var next = rb.Name == "AgentOperationModeExternal"
|
|
? OperationModePolicy.ExternalMode
|
|
: OperationModePolicy.InternalMode;
|
|
|
|
var app = System.Windows.Application.Current as App;
|
|
var settings = app?.SettingsService?.Settings;
|
|
if (settings == null)
|
|
return;
|
|
|
|
var current = OperationModePolicy.Normalize(settings.OperationMode);
|
|
if (string.Equals(next, current, StringComparison.OrdinalIgnoreCase))
|
|
return;
|
|
|
|
var ok = PromptPasswordDialog(
|
|
"운영 모드 변경 ? 비밀번호 확인",
|
|
"\U0001f512 사내/사외 모드 변경",
|
|
"비밀번호를 입력하세요:");
|
|
if (!ok)
|
|
{
|
|
ApplyOperationModeState(settings.OperationMode);
|
|
return;
|
|
}
|
|
|
|
settings.OperationMode = next;
|
|
_vm.OperationMode = next;
|
|
app?.SettingsService?.Save();
|
|
ApplyOperationModeState(next);
|
|
}
|
|
|
|
private void AgentContextTokensCard_Checked(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true)
|
|
return;
|
|
|
|
_vm.LlmMaxContextTokens = rb.Name switch
|
|
{
|
|
"AgentContextTokens16K" => 16384,
|
|
"AgentContextTokens64K" => 65536,
|
|
"AgentContextTokens256K" => 262144,
|
|
"AgentContextTokens1M" => 1_000_000,
|
|
_ => 4096,
|
|
};
|
|
}
|
|
|
|
private void AgentRetentionDaysCard_Checked(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true)
|
|
return;
|
|
|
|
_vm.LlmRetentionDays = rb.Name switch
|
|
{
|
|
"AgentRetentionDays30" => 30,
|
|
"AgentRetentionDays90" => 90,
|
|
"AgentRetentionDaysUnlimited" => 0,
|
|
_ => 7,
|
|
};
|
|
}
|
|
|
|
private void AgentLogLevelCard_Checked(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!IsLoaded || sender is not RadioButton rb || rb.IsChecked != true)
|
|
return;
|
|
|
|
_vm.AgentLogLevel = rb.Name switch
|
|
{
|
|
"AgentLogLevelDetailed" => "detailed",
|
|
"AgentLogLevelDebug" => "debug",
|
|
_ => "simple",
|
|
};
|
|
}
|
|
|
|
private void BuildServiceModelPanels()
|
|
{
|
|
BuildRegisteredModelList("ollama", OllamaRegisteredModelsList);
|
|
BuildRegisteredModelList("vllm", VllmRegisteredModelsList);
|
|
BuildServiceModelChips("ollama", OllamaModelChipPanel, _vm.OllamaModel, value => _vm.OllamaModel = value);
|
|
BuildServiceModelChips("vllm", VllmModelChipPanel, _vm.VllmModel, value => _vm.VllmModel = value);
|
|
BuildPresetModelChips(
|
|
GeminiModelChipPanel,
|
|
_vm.GeminiModel,
|
|
value => _vm.GeminiModel = value,
|
|
new (string Id, string Label, string Hint)[]
|
|
{
|
|
("gemini-3.1-pro-preview", "Gemini 3.1 Pro", "최고 성능"),
|
|
("gemini-3-flash", "Gemini 3 Flash", "빠른 응답"),
|
|
("gemini-3.1-flash-lite-preview", "Gemini 3.1 Flash Lite", "경량"),
|
|
("gemini-2.5-flash", "Gemini 2.5 Flash", "균형"),
|
|
("gemini-2.5-flash-lite", "Gemini 2.5 Flash Lite", "저비용"),
|
|
});
|
|
BuildPresetModelChips(
|
|
ClaudeModelChipPanel,
|
|
_vm.ClaudeModel,
|
|
value => _vm.ClaudeModel = value,
|
|
new (string Id, string Label, string Hint)[]
|
|
{
|
|
(ProviderModelIds.SigmoidOpus46, "Claude Opus 4.6", "최고 성능"),
|
|
(ProviderModelIds.SigmoidSonnet46, "Claude Sonnet 4.6", "균형"),
|
|
(ProviderModelIds.SigmoidHaiku45_20251001, "Claude Haiku 4.5", "경량"),
|
|
(ProviderModelIds.SigmoidSonnet45_20250929, "Claude Sonnet 4.5", "이전 안정판"),
|
|
(ProviderModelIds.SigmoidOpus4_20250514, "Claude Opus 4", "이전 최고급"),
|
|
});
|
|
}
|
|
|
|
private void BuildRegisteredModelList(string service, ItemsControl? target)
|
|
{
|
|
if (target == null) return;
|
|
var view = new ListCollectionView(_vm.RegisteredModels);
|
|
view.Filter = item =>
|
|
{
|
|
if (item is not RegisteredModelRow row) return false;
|
|
return string.Equals(row.Service, service, StringComparison.OrdinalIgnoreCase);
|
|
};
|
|
target.ItemsSource = view;
|
|
}
|
|
|
|
private void BuildServiceModelChips(string service, WrapPanel? panel, string selectedValue, Action<string> applySelection)
|
|
{
|
|
if (panel == null)
|
|
return;
|
|
|
|
panel.Children.Clear();
|
|
var models = _vm.RegisteredModels
|
|
.Where(row => string.Equals(row.Service, service, StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
|
|
if (models.Count == 0)
|
|
{
|
|
panel.Children.Add(new Border
|
|
{
|
|
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke,
|
|
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(10),
|
|
Padding = new Thickness(12, 8, 12, 8),
|
|
Child = new TextBlock
|
|
{
|
|
Text = "등록된 모델이 없습니다. + 모델 추가로 먼저 등록하세요.",
|
|
FontSize = 11.5,
|
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
foreach (var model in models)
|
|
{
|
|
var isSelected = string.Equals(model.EncryptedModelName, selectedValue, StringComparison.OrdinalIgnoreCase);
|
|
var chip = new Border
|
|
{
|
|
Cursor = Cursors.Hand,
|
|
CornerRadius = new CornerRadius(10),
|
|
BorderThickness = new Thickness(1),
|
|
BorderBrush = isSelected
|
|
? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue)
|
|
: (TryFindResource("BorderColor") as Brush ?? Brushes.LightGray),
|
|
Background = isSelected
|
|
? (TryFindResource("ItemHoverBackground") as Brush ?? Brushes.AliceBlue)
|
|
: Brushes.Transparent,
|
|
Padding = new Thickness(12, 9, 12, 9),
|
|
Margin = new Thickness(0, 0, 8, 8),
|
|
Child = new StackPanel
|
|
{
|
|
Children =
|
|
{
|
|
new TextBlock
|
|
{
|
|
Text = string.IsNullOrWhiteSpace(model.Alias) ? "(이름 없음)" : model.Alias,
|
|
FontSize = 12,
|
|
FontWeight = isSelected ? FontWeights.SemiBold : FontWeights.Normal,
|
|
Foreground = isSelected
|
|
? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue)
|
|
: (TryFindResource("PrimaryText") as Brush ?? Brushes.Black),
|
|
},
|
|
new TextBlock
|
|
{
|
|
Text = string.IsNullOrWhiteSpace(model.Endpoint) ? "기본 서버 사용" : model.Endpoint,
|
|
FontSize = 10.5,
|
|
Margin = new Thickness(0, 3, 0, 0),
|
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
var captured = model.EncryptedModelName;
|
|
chip.MouseLeftButtonUp += (_, _) =>
|
|
{
|
|
applySelection(captured);
|
|
BuildServiceModelPanels();
|
|
};
|
|
panel.Children.Add(chip);
|
|
}
|
|
}
|
|
|
|
private void BuildPresetModelChips(
|
|
WrapPanel? panel,
|
|
string? selectedValue,
|
|
Action<string> applySelection,
|
|
IEnumerable<(string Id, string Label, string Hint)> models)
|
|
{
|
|
if (panel == null)
|
|
return;
|
|
|
|
panel.Children.Clear();
|
|
foreach (var model in models)
|
|
{
|
|
var isSelected = string.Equals(model.Id, selectedValue, StringComparison.OrdinalIgnoreCase);
|
|
var chip = new Border
|
|
{
|
|
Cursor = Cursors.Hand,
|
|
CornerRadius = new CornerRadius(12),
|
|
BorderThickness = new Thickness(1),
|
|
BorderBrush = isSelected
|
|
? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue)
|
|
: (TryFindResource("BorderColor") as Brush ?? Brushes.LightGray),
|
|
Background = isSelected
|
|
? (TryFindResource("ItemHoverBackground") as Brush ?? Brushes.AliceBlue)
|
|
: Brushes.Transparent,
|
|
Padding = new Thickness(12, 9, 12, 9),
|
|
Margin = new Thickness(0, 0, 8, 8),
|
|
Child = new StackPanel
|
|
{
|
|
Children =
|
|
{
|
|
new TextBlock
|
|
{
|
|
Text = model.Label,
|
|
FontSize = 12,
|
|
FontWeight = isSelected ? FontWeights.SemiBold : FontWeights.Normal,
|
|
Foreground = isSelected
|
|
? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue)
|
|
: (TryFindResource("PrimaryText") as Brush ?? Brushes.Black),
|
|
},
|
|
new TextBlock
|
|
{
|
|
Text = model.Hint,
|
|
FontSize = 10.5,
|
|
Margin = new Thickness(0, 3, 0, 0),
|
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
var capturedId = model.Id;
|
|
chip.MouseLeftButtonUp += (_, _) =>
|
|
{
|
|
applySelection(capturedId);
|
|
BuildServiceModelPanels();
|
|
};
|
|
panel.Children.Add(chip);
|
|
}
|
|
}
|
|
|
|
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 == UnifiedAdminPassword)
|
|
dlg.DialogResult = true;
|
|
else
|
|
{
|
|
pwBox.Clear();
|
|
pwBox.Focus();
|
|
}
|
|
};
|
|
btnRow.Children.Add(okBtn);
|
|
stack.Children.Add(btnRow);
|
|
border.Child = stack;
|
|
dlg.Content = border;
|
|
dlg.Loaded += (_, _) => pwBox.Focus();
|
|
|
|
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)
|
|
{
|
|
ApplyMainTabVisibility(NormalizeDisplayMode((System.Windows.Application.Current as App)?.SettingsService?.Settings?.Llm?.AgentUiExpressionLevel));
|
|
}
|
|
|
|
public void SelectAgentSettingsTab()
|
|
{
|
|
OpenAgentSettingsShortcut(closeAfterOpen: false);
|
|
}
|
|
|
|
private void BtnAgentSettingsBack_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (MainSettingsTab == null)
|
|
return;
|
|
|
|
var fallback = MainSettingsTab.Items
|
|
.OfType<TabItem>()
|
|
.FirstOrDefault(t => t != AgentTabItem && t.Visibility == Visibility.Visible);
|
|
|
|
if (fallback != null)
|
|
MainSettingsTab.SelectedItem = fallback;
|
|
}
|
|
|
|
private void BtnAgentShortcut_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
OpenAgentSettingsShortcut(closeAfterOpen: true);
|
|
}
|
|
|
|
private void MainSettingsTab_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (MainSettingsTab?.SelectedItem is not TabItem selected)
|
|
return;
|
|
|
|
if (!ReferenceEquals(selected, AgentShortcutTabItem))
|
|
return;
|
|
|
|
OpenAgentSettingsShortcut(closeAfterOpen: true);
|
|
}
|
|
|
|
private void OpenAgentSettingsShortcut(bool closeAfterOpen)
|
|
{
|
|
var app = System.Windows.Application.Current as App;
|
|
app?.OpenAgentSettingsInChat();
|
|
|
|
if (closeAfterOpen)
|
|
Close();
|
|
}
|
|
|
|
private void ApplyOperationModeState(string? mode)
|
|
{
|
|
var normalized = OperationModePolicy.Normalize(mode);
|
|
if (AgentOperationModeInternal != null) AgentOperationModeInternal.IsChecked = normalized == OperationModePolicy.InternalMode;
|
|
if (AgentOperationModeExternal != null) AgentOperationModeExternal.IsChecked = normalized == OperationModePolicy.ExternalMode;
|
|
}
|
|
|
|
private static void SyncOperationModeCombo(ComboBox? combo, string normalized)
|
|
{
|
|
if (combo == null)
|
|
return;
|
|
|
|
foreach (var item in combo.Items.OfType<ComboBoxItem>())
|
|
{
|
|
var key = item.Tag as string;
|
|
if (string.Equals(key, normalized, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (!ReferenceEquals(combo.SelectedItem, item))
|
|
combo.SelectedItem = item;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool PromptPasswordDialog(string title, string header, string message)
|
|
{
|
|
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 = 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 = header,
|
|
FontSize = 15,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = fgBrush,
|
|
Margin = new Thickness(0, 0, 0, 12),
|
|
});
|
|
stack.Children.Add(new TextBlock
|
|
{
|
|
Text = message,
|
|
FontSize = 12,
|
|
Foreground = subFgBrush,
|
|
Margin = new Thickness(0, 0, 0, 6),
|
|
});
|
|
var pwBox = new PasswordBox
|
|
{
|
|
FontSize = 14,
|
|
Padding = new Thickness(8, 6, 8, 6),
|
|
Background = itemBg,
|
|
Foreground = fgBrush,
|
|
BorderBrush = borderBrush,
|
|
PasswordChar = '*',
|
|
};
|
|
stack.Children.Add(pwBox);
|
|
|
|
var btnRow = new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
Margin = new Thickness(0, 16, 0, 0),
|
|
};
|
|
var cancelBtn = new Button { Content = "취소", Padding = new Thickness(16, 6, 16, 6), Margin = new Thickness(0, 0, 8, 0) };
|
|
cancelBtn.Click += (_, _) => { dlg.DialogResult = false; };
|
|
btnRow.Children.Add(cancelBtn);
|
|
var okBtn = new Button { Content = "확인", Padding = new Thickness(16, 6, 16, 6), IsDefault = true };
|
|
okBtn.Click += (_, _) =>
|
|
{
|
|
if (pwBox.Password == UnifiedAdminPassword)
|
|
dlg.DialogResult = true;
|
|
else
|
|
{
|
|
pwBox.Clear();
|
|
pwBox.Focus();
|
|
}
|
|
};
|
|
btnRow.Children.Add(okBtn);
|
|
stack.Children.Add(btnRow);
|
|
border.Child = stack;
|
|
dlg.Content = border;
|
|
dlg.Loaded += (_, _) => pwBox.Focus();
|
|
return dlg.ShowDialog() == true;
|
|
}
|
|
|
|
private void AiEnabled_Changed(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!IsLoaded) return;
|
|
var sourceToggle = sender as CheckBox;
|
|
var tryEnable = sourceToggle?.IsChecked == true;
|
|
|
|
// 비활성화는 즉시 적용 (비밀번호 불필요)
|
|
if (!tryEnable)
|
|
{
|
|
var app2 = System.Windows.Application.Current as App;
|
|
if (app2?.SettingsService?.Settings != null)
|
|
{
|
|
app2.SettingsService.Settings.AiEnabled = false;
|
|
app2.SettingsService.Save();
|
|
}
|
|
ApplyAiEnabledState(false);
|
|
return;
|
|
}
|
|
|
|
// 이미 활성화된 상태에서 설정 창이 열릴 때 토글 복원으로 인한 이벤트 → 비밀번호 불필요
|
|
var currentApp = System.Windows.Application.Current as App;
|
|
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 == UnifiedAdminPassword)
|
|
dlg.DialogResult = true;
|
|
else
|
|
{
|
|
pwBox.Clear();
|
|
pwBox.Focus();
|
|
}
|
|
};
|
|
btnRow.Children.Add(okBtn);
|
|
stack.Children.Add(btnRow);
|
|
border.Child = stack;
|
|
dlg.Content = border;
|
|
dlg.Loaded += (_, _) => pwBox.Focus();
|
|
|
|
if (dlg.ShowDialog() == true)
|
|
{
|
|
var app = System.Windows.Application.Current as App;
|
|
if (app?.SettingsService?.Settings != null)
|
|
{
|
|
app.SettingsService.Settings.AiEnabled = true;
|
|
app.SettingsService.Save();
|
|
}
|
|
ApplyAiEnabledState(true);
|
|
}
|
|
else
|
|
{
|
|
// 취소/실패 ? 토글 원상복구
|
|
if (AgentAiEnabledToggle != null) AgentAiEnabledToggle.IsChecked = false;
|
|
}
|
|
}
|
|
|
|
private void OperationModeCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (!IsLoaded) return;
|
|
if (sender is not ComboBox combo || combo.SelectedItem is not ComboBoxItem selected) return;
|
|
var mode = selected.Tag as string;
|
|
if (string.IsNullOrWhiteSpace(mode)) return;
|
|
|
|
var app = System.Windows.Application.Current as App;
|
|
var settings = app?.SettingsService?.Settings;
|
|
if (settings == null) return;
|
|
|
|
var normalized = OperationModePolicy.Normalize(mode);
|
|
var current = OperationModePolicy.Normalize(settings.OperationMode);
|
|
if (string.Equals(normalized, current, StringComparison.OrdinalIgnoreCase))
|
|
return;
|
|
|
|
var ok = PromptPasswordDialog(
|
|
"운영 모드 변경 ? 비밀번호 확인",
|
|
"\U0001f512 사내/사외 모드 변경",
|
|
"비밀번호를 입력하세요:");
|
|
if (!ok)
|
|
{
|
|
ApplyOperationModeState(settings.OperationMode);
|
|
return;
|
|
}
|
|
|
|
settings.OperationMode = normalized;
|
|
app?.SettingsService?.Save();
|
|
}
|
|
|
|
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 == UnifiedAdminPassword)
|
|
dlg.DialogResult = true;
|
|
else
|
|
{
|
|
pwBox.Clear();
|
|
pwBox.Focus();
|
|
}
|
|
};
|
|
btnRow.Children.Add(okBtn);
|
|
stack.Children.Add(btnRow);
|
|
border.Child = stack;
|
|
dlg.Content = border;
|
|
dlg.Loaded += (_, _) => pwBox.Focus();
|
|
|
|
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 = System.Windows.Application.Current as App;
|
|
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 = new SolidColorBrush((Color)ColorConverter.ConvertFromString(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 @modelcontextprotocol/server-filesystem");
|
|
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 { }
|
|
}
|
|
|
|
// ─── 폴백/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();
|
|
if (svc == string.Concat("cl", "aude"))
|
|
svc = "claude";
|
|
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();
|
|
if (svc == string.Concat("cl", "aude"))
|
|
svc = "claude";
|
|
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[]
|
|
{
|
|
string.Concat("cl", "aude-sonnet-4-6"),
|
|
string.Concat("cl", "aude-opus-4-6"),
|
|
string.Concat("cl", "aude-haiku-4-5"),
|
|
string.Concat("cl", "aude-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 { 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 { /* 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 { }
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
|