From c9a6e6442fb60111759e39c4fcf65ccba60708bd Mon Sep 17 00:00:00 2001 From: lacvet Date: Fri, 3 Apr 2026 19:37:24 +0900 Subject: [PATCH] =?UTF-8?q?[Phase=2041]=20SettingsViewModel=C2=B7AgentLoop?= =?UTF-8?q?Service=20=ED=8C=8C=EC=85=9C=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EB=B6=84=ED=95=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SettingsViewModel (1,855줄 → 320줄, 82.7% 감소): - SettingsViewModel.Properties.cs (837줄): 바인딩 프로퍼티 전체 - SettingsViewModel.Methods.cs (469줄): Save/Browse/Add 등 메서드 - SettingsViewModelModels.cs (265줄): 6개 모델 클래스 분리 AgentLoopService (1,823줄 → 1,334줄, 26.8% 감소): - AgentLoopService.Execution.cs (498줄): 병렬 도구 실행, ToolExecutionState - 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 --- docs/NEXT_ROADMAP.md | 29 +- .../Agent/AgentLoopService.Execution.cs | 498 ++++++ .../Services/Agent/AgentLoopService.cs | 491 +----- .../ViewModels/SettingsViewModel.Methods.cs | 469 +++++ .../SettingsViewModel.Properties.cs | 837 +++++++++ src/AxCopilot/ViewModels/SettingsViewModel.cs | 1537 +---------------- .../ViewModels/SettingsViewModelModels.cs | 265 +++ 7 files changed, 2099 insertions(+), 2027 deletions(-) create mode 100644 src/AxCopilot/Services/Agent/AgentLoopService.Execution.cs create mode 100644 src/AxCopilot/ViewModels/SettingsViewModel.Methods.cs create mode 100644 src/AxCopilot/ViewModels/SettingsViewModel.Properties.cs create mode 100644 src/AxCopilot/ViewModels/SettingsViewModelModels.cs diff --git a/docs/NEXT_ROADMAP.md b/docs/NEXT_ROADMAP.md index 81f83b8..f463fd7 100644 --- a/docs/NEXT_ROADMAP.md +++ b/docs/NEXT_ROADMAP.md @@ -4605,5 +4605,32 @@ ThemeResourceHelper에 5개 정적 필드 추가: --- -최종 업데이트: 2026-04-03 (Phase 22~40 구현 완료 — CC 동등성 37/37 + 코드 품질 리팩터링 8차) +## Phase 41 — SettingsViewModel·AgentLoopService 파셜 분할 (v2.3) ✅ 완료 + +> **목표**: SettingsViewModel (1,855줄)·AgentLoopService (1,823줄) 파셜 클래스 분할. + +### SettingsViewModel 분할 + +| 파일 | 줄 수 | 내용 | +|------|-------|------| +| `SettingsViewModel.cs` (메인) | 320 | 클래스 선언, 필드, 이벤트, 생성자 | +| `SettingsViewModel.Properties.cs` | 837 | 바인딩 프로퍼티 전체 (LLM, 런처, 기능토글, 테마 등) | +| `SettingsViewModel.Methods.cs` | 469 | Save, Browse, AddShortcut, AddSnippet 등 메서드 | +| `SettingsViewModelModels.cs` | 265 | ThemeCardModel, ColorRowModel, SnippetRowModel 등 6개 모델 클래스 | + +- **메인 파일**: 1,855줄 → 320줄 (**82.7% 감소**) + +### AgentLoopService 분할 + +| 파일 | 줄 수 | 내용 | +|------|-------|------| +| `AgentLoopService.cs` (메인) | 1,334 | 상수, 필드, 생성자, RunAsync 루프, 헬퍼 | +| `AgentLoopService.Execution.cs` | 498 | TruncateOutput, ReadOnlyTools, ClassifyToolCalls, ToolExecutionState, ParallelState, ExecuteToolsInParallelAsync | + +- **메인 파일**: 1,823줄 → 1,334줄 (**26.8% 감소**) +- **빌드**: 경고 0, 오류 0 + +--- + +최종 업데이트: 2026-04-03 (Phase 22~41 구현 완료 — CC 동등성 37/37 + 코드 품질 리팩터링 9차) diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.Execution.cs b/src/AxCopilot/Services/Agent/AgentLoopService.Execution.cs new file mode 100644 index 0000000..aa84f7e --- /dev/null +++ b/src/AxCopilot/Services/Agent/AgentLoopService.Execution.cs @@ -0,0 +1,498 @@ +using System.Diagnostics; +using System.Text.Json; +using AxCopilot.Models; +using AxCopilot.Services; + +namespace AxCopilot.Services.Agent; + +public partial class AgentLoopService +{ + private static string TruncateOutput(string output, int maxLength) + { + if (output.Length <= maxLength) return output; + return output[..maxLength] + "\n... (출력 잘림)"; + } + + // ─── 병렬 도구 실행 ────────────────────────────────────────────────── + + // 읽기 전용 도구 (파일 상태를 변경하지 않음) + private static readonly HashSet ReadOnlyTools = new(StringComparer.OrdinalIgnoreCase) + { + "file_read", "glob", "grep_tool", "folder_map", "document_read", + "search_codebase", "code_search", "env_tool", "datetime_tool", + "dev_env_detect", "memory", "skill_manager", "json_tool", + "regex_tool", "base64_tool", "hash_tool", "image_analyze", + }; + + /// 도구 호출을 병렬 가능 / 순차 필수로 분류합니다. + private static (List Parallel, List Sequential) + ClassifyToolCalls(List calls) + { + var parallel = new List(); + var sequential = new List(); + + foreach (var call in calls) + { + if (ReadOnlyTools.Contains(call.ToolName ?? "")) + parallel.Add(call); + else + sequential.Add(call); + } + + // 읽기 전용 도구가 1개뿐이면 병렬화 의미 없음 + if (parallel.Count <= 1) + { + sequential.InsertRange(0, parallel); + parallel.Clear(); + } + + return (parallel, sequential); + } + + // ─── Phase 33-B: 도구 실행 루프 상태 (RunAsync에서 추출) ───────────── + /// 도구 실행 루프의 가변 상태. 메서드 간 공유. + private class ToolExecutionState + { + public int CurrentStep; + public int TotalToolCalls; + public int MaxIterations; + public int BaseMax; + public int MaxRetry; + public int ConsecutiveErrors; + public int StatsSuccessCount; + public int StatsFailCount; + public int StatsInputTokens; + public int StatsOutputTokens; + public List StatsUsedTools = new(); + public List PlanSteps = new(); + public bool DocumentPlanCalled; + public string? DocumentPlanPath; + public string? DocumentPlanTitle; + public string? DocumentPlanScaffold; + } + + /// 병렬 실행용 가변 상태 (ToolExecutionState의 서브셋). TODO: ToolExecutionState로 통합 예정. + private class ParallelState + { + public int CurrentStep; + public int TotalToolCalls; + public int MaxIterations; + public int ConsecutiveErrors; + public int StatsSuccessCount; + public int StatsFailCount; + public int StatsInputTokens; + public int StatsOutputTokens; + } + + /// + /// Phase 33-B: 단일 도구 호출을 처리합니다 (RunAsync에서 추출). + /// 훅 실행 → 도구 실행 → 결과 처리 → 검증까지 단일 도구 라이프사이클. + /// + /// LoopAction: Continue=다음 도구, Break=루프 중단, Return=값 반환. + private async Task<(ToolCallAction Action, string? ReturnValue)> ProcessSingleToolCallAsync( + LlmService.ContentBlock call, + List messages, + AgentContext context, + ToolExecutionState state, + List toolCalls, + string activeTabSnapshot, + int iteration, + CancellationToken ct) + { + var llm = _settings.Settings.Llm; + var tool = _tools.Get(call.ToolName); + if (tool == null) + { + var errResult = $"알 수 없는 도구: {call.ToolName}"; + EmitEvent(AgentEventType.Error, call.ToolName, errResult); + messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, errResult)); + return (ToolCallAction.Continue, null); + } + + // Task Decomposition: 단계 진행률 추적 + if (state.PlanSteps.Count > 0) + { + var summary = FormatToolCallSummary(call); + var newStep = TaskDecomposer.EstimateCurrentStep( + state.PlanSteps, call.ToolName, summary, state.CurrentStep); + if (newStep != state.CurrentStep) + { + state.CurrentStep = newStep; + EmitEvent(AgentEventType.StepStart, "", state.PlanSteps[state.CurrentStep], + stepCurrent: state.CurrentStep + 1, stepTotal: state.PlanSteps.Count); + } + } + + // 개발자 모드: 도구 호출 파라미터 상세 표시 + if (context.DevMode) + { + var paramJson = call.ToolInput?.ToString() ?? "{}"; + if (paramJson.Length > 500) paramJson = paramJson[..500] + "..."; + EmitEvent(AgentEventType.Thinking, call.ToolName, + $"[DEV] 도구 호출: {call.ToolName}\n파라미터: {paramJson}"); + } + + // 이벤트 로그 기록 + if (_eventLog != null) + _ = _eventLog.AppendAsync(AgentEventLogType.ToolRequest, + JsonSerializer.Serialize(new { toolName = call.ToolName, iteration })); + + EmitEvent(AgentEventType.ToolCall, call.ToolName, FormatToolCallSummary(call)); + + // 개발자 모드: 스텝 바이 스텝 승인 + if (context.DevModeStepApproval && UserDecisionCallback != null) + { + var decision = await UserDecisionCallback( + $"[DEV] 도구 '{call.ToolName}' 실행을 승인하시겠습니까?\n{FormatToolCallSummary(call)}", + new List { "승인", "건너뛰기", "중단" }); + if (decision == "중단") + { + EmitEvent(AgentEventType.Complete, "", "[DEV] 사용자가 실행을 중단했습니다"); + return (ToolCallAction.Return, "사용자가 개발자 모드에서 실행을 중단했습니다."); + } + if (decision == "건너뛰기") + { + messages.Add(LlmService.CreateToolResultMessage( + call.ToolId, call.ToolName, "[SKIPPED by developer] 사용자가 이 도구 실행을 건너뛰었습니다.")); + return (ToolCallAction.Continue, null); + } + } + + // 영향 범위 기반 의사결정 체크 + var decisionRequired = CheckDecisionRequired(call, context); + if (decisionRequired != null && UserDecisionCallback != null) + { + var decision = await UserDecisionCallback( + decisionRequired, + new List { "승인", "건너뛰기", "취소" }); + if (decision == "취소") + { + EmitEvent(AgentEventType.Complete, "", "사용자가 작업을 취소했습니다"); + return (ToolCallAction.Return, "사용자가 작업을 취소했습니다."); + } + if (decision == "건너뛰기") + { + messages.Add(LlmService.CreateToolResultMessage( + call.ToolId, call.ToolName, "[SKIPPED] 사용자가 이 작업을 건너뛰었습니다.")); + return (ToolCallAction.Continue, null); + } + } + + // ── Pre-Hook 실행 ── + await RunToolHooksAsync(llm, call, context, "pre", ct); + + // ── 도구 실행 ── + ToolResult result; + var sw = Stopwatch.StartNew(); + try + { + var input = call.ToolInput ?? JsonDocument.Parse("{}").RootElement; + result = await tool.ExecuteAsync(input, context, ct); + } + catch (OperationCanceledException) + { + EmitEvent(AgentEventType.Complete, "", "사용자가 작업을 취소했습니다."); + return (ToolCallAction.Return, "사용자가 작업을 취소했습니다."); + } + catch (Exception ex) + { + result = ToolResult.Fail($"도구 실행 오류: {ex.Message}"); + } + sw.Stop(); + + // ── Post-Hook 실행 ── + await RunToolHooksAsync(llm, call, context, "post", ct, result); + + // 개발자 모드: 도구 결과 상세 표시 + if (context.DevMode) + { + EmitEvent(AgentEventType.Thinking, call.ToolName, + $"[DEV] 결과: {(result.Success ? "성공" : "실패")}\n{TruncateOutput(result.Output, 500)}"); + } + + var tokenUsage = _llm.LastTokenUsage; + EmitEvent( + result.Success ? AgentEventType.ToolResult : AgentEventType.Error, + call.ToolName, + TruncateOutput(result.Output, 200), + result.FilePath, + elapsedMs: sw.ElapsedMilliseconds, + inputTokens: tokenUsage?.PromptTokens ?? 0, + outputTokens: tokenUsage?.CompletionTokens ?? 0, + toolInput: call.ToolInput?.ToString(), + iteration: iteration); + + // 통계 수집 + if (result.Success) state.StatsSuccessCount++; else state.StatsFailCount++; + state.StatsInputTokens += tokenUsage?.PromptTokens ?? 0; + state.StatsOutputTokens += tokenUsage?.CompletionTokens ?? 0; + if (!state.StatsUsedTools.Contains(call.ToolName)) + state.StatsUsedTools.Add(call.ToolName); + + // 감사 로그 기록 + if (llm.EnableAuditLog) + { + AuditLogService.LogToolCall( + _conversationId, activeTabSnapshot, + call.ToolName, + call.ToolInput.ToString() ?? "", + TruncateOutput(result.Output, 500), + result.FilePath, result.Success); + } + + // 이벤트 로그 + if (_eventLog != null) + _ = _eventLog.AppendAsync(AgentEventLogType.ToolResult, + JsonSerializer.Serialize(new + { + toolName = call.ToolName, + success = result.Success, + outputLength = result.Output?.Length ?? 0 + })); + + state.TotalToolCalls++; + + // 동적 반복 한도 확장 + if (state.TotalToolCalls > Defaults.ToolCallThresholdForExpansion && state.MaxIterations < state.BaseMax * 2) + state.MaxIterations = Math.Min(state.BaseMax * 2, 50); + + // 테스트-수정 루프 예산 확장 + if (call.ToolName == "test_loop" && (result.Output?.Contains("[AUTO_FIX:") ?? false)) + { + var testFixMax = llm.MaxTestFixIterations > 0 ? llm.MaxTestFixIterations : Defaults.MaxTestFixIterations; + var testFixBudget = state.BaseMax + testFixMax * 3; + if (state.MaxIterations < testFixBudget) + state.MaxIterations = Math.Min(testFixBudget, 60); + } + + // UI 스레드가 이벤트를 렌더링할 시간 확보 + await Task.Delay(80, ct); + + // Self-Reflection: 도구 실패 시 반성 프롬프트 추가 + if (!result.Success) + { + state.ConsecutiveErrors++; + if (state.ConsecutiveErrors <= state.MaxRetry) + { + var reflectionMsg = $"[Tool '{call.ToolName}' failed: {TruncateOutput(result.Output ?? "", 500)}]\n" + + $"Analyze why this failed. Consider: wrong parameters, wrong file path, missing prerequisites. " + + $"Try a different approach. (Error {state.ConsecutiveErrors}/{state.MaxRetry})"; + messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, reflectionMsg)); + EmitEvent(AgentEventType.Thinking, "", $"Self-Reflection: 실패 분석 후 재시도 ({state.ConsecutiveErrors}/{state.MaxRetry})"); + } + else + { + messages.Add(LlmService.CreateToolResultMessage( + call.ToolId, call.ToolName, + $"[FAILED after {state.MaxRetry} retries] {TruncateOutput(result.Output ?? "", 500)}\n" + + "Stop retrying this tool. Explain the error to the user and suggest alternative approaches.")); + + // 에이전트 메모리에 실패 원인 저장 + try + { + var memSvc = CurrentApp?.MemoryService; + if (memSvc != null && (_settings.Settings.Llm.EnableAgentMemory)) + { + memSvc.Add("correction", + $"도구 '{call.ToolName}' 반복 실패: {TruncateOutput(result.Output ?? "", Defaults.LogSummaryMaxLength)}", + $"conv:{_conversationId}", context.WorkFolder); + } + } + catch (Exception ex) { LogService.Warn($"[AgentLoop] 에이전트 메모리 저장 실패: {ex.Message}"); } + } + } + else + { + state.ConsecutiveErrors = 0; + + // ToolResultSizer 적용 + var sizedResult = ToolResultSizer.Apply(result.Output ?? "", call.ToolName); + messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, sizedResult.Output)); + + // document_plan 호출 플래그 + 메타데이터 저장 + if (call.ToolName == "document_plan" && result.Success) + { + state.DocumentPlanCalled = true; + try + { + var planJson = JsonDocument.Parse(result.Output ?? "{}"); + if (planJson.RootElement.TryGetProperty("path", out var p)) + state.DocumentPlanPath = p.GetString(); + if (planJson.RootElement.TryGetProperty("title", out var t)) + state.DocumentPlanTitle = t.GetString(); + if (planJson.RootElement.TryGetProperty("body", out var b)) + state.DocumentPlanScaffold = b.GetString(); + } + catch (Exception) { /* document_plan JSON 파싱 실패 — 선택적 메타데이터이므로 무시 */ } + + // html_create 즉시 호출 유도 + var toolHint = (result.Output?.Contains("document_assemble") ?? false) ? "document_assemble" : + (result.Output?.Contains("file_write") ?? false) ? "file_write" : "html_create"; + messages.Add(new ChatMessage { Role = "user", + Content = $"document_plan이 완료되었습니다. " + + $"위 결과의 body/sections의 [내용...] 부분을 실제 상세 내용으로 모두 채워서 " + + $"{toolHint} 도구를 지금 즉시 호출하세요. " + + $"각 섹션마다 반드시 충분한 내용을 작성하고, 설명 없이 도구를 바로 호출하세요." }); + EmitEvent(AgentEventType.Thinking, "", $"문서 개요 완성 — {toolHint} 호출 중..."); + } + + // 단일 문서 생성 도구 성공 → 즉시 루프 종료 + if (result.Success && IsTerminalDocumentTool(call.ToolName) && toolCalls.Count == 1) + { + var shouldVerify2 = activeTabSnapshot == "Code" + ? llm.Code.EnableCodeVerification && IsCodeVerificationTarget(call.ToolName) + : llm.EnableCoworkVerification && IsDocumentCreationTool(call.ToolName); + if (shouldVerify2) + { + await RunPostToolVerificationAsync(messages, call.ToolName, result, context, ct); + } + EmitEvent(AgentEventType.Complete, "", "에이전트 작업 완료"); + return (ToolCallAction.Return, result.Output ?? ""); + } + + // Post-Tool Verification: 탭별 검증 강제 + var shouldVerify = activeTabSnapshot == "Code" + ? llm.Code.EnableCodeVerification && IsCodeVerificationTarget(call.ToolName) + : llm.EnableCoworkVerification && IsDocumentCreationTool(call.ToolName); + if (shouldVerify && result.Success) + { + await RunPostToolVerificationAsync(messages, call.ToolName, result, context, ct); + } + } + + return (ToolCallAction.Continue, null); + } + + /// 도구 호출 처리 후 루프 제어 액션. + private enum ToolCallAction { Continue, Break, Return } + + /// Phase 33-B: 도구 훅(Pre/Post) 실행 헬퍼. + private async Task RunToolHooksAsync( + Models.LlmSettings llm, LlmService.ContentBlock call, + AgentContext context, string phase, CancellationToken ct, + ToolResult? result = null) + { + if (!llm.EnableToolHooks || llm.AgentHooks.Count == 0) return; + try + { + var hookResults = await AgentHookRunner.RunAsync( + llm.AgentHooks, call.ToolName, phase, + toolInput: call.ToolInput.ToString(), + toolOutput: result != null ? TruncateOutput(result.Output, 2048) : null, + success: result?.Success ?? false, + workFolder: context.WorkFolder, + timeoutMs: llm.ToolHookTimeoutMs, + ct: ct); + foreach (var pr in hookResults.Where(r => !r.Success)) + EmitEvent(AgentEventType.Error, call.ToolName, $"[Hook:{pr.HookName}] {pr.Output}"); + } + catch (Exception ex) { LogService.Warn($"[AgentLoop] {phase}ToolUse 훅 실패: {ex.Message}"); } + } + + /// DelegateAgentTool에서 호출하는 서브에이전트 실행기. + private async Task RunSubAgentAsync(string agentType, string task, string workFolder, CancellationToken ct) + { + try + { + var systemPrompt = $"당신은 전문 {agentType} 에이전트입니다. 주어진 작업을 수행하세요."; + var msgs = new List + { + new() { Role = "system", Content = systemPrompt }, + new() { Role = "user", Content = task } + }; + + // 서브에이전트는 간단한 텍스트 응답으로 처리 (도구 호출 없이) + var response = await _llm.SendAsync(msgs, ct); + return response; + } + catch (Exception ex) + { + return $"서브에이전트 실행 오류: {ex.Message}"; + } + } + + /// 읽기 전용 도구들을 병렬 실행합니다. + private async Task ExecuteToolsInParallelAsync( + List calls, + List messages, + AgentContext context, + List planSteps, + ParallelState state, + int baseMax, int maxRetry, + Models.LlmSettings llm, + int iteration, + CancellationToken ct, + List statsUsedTools) + { + EmitEvent(AgentEventType.Thinking, "", $"읽기 전용 도구 {calls.Count}개를 병렬 실행 중..."); + + var tasks = calls.Select(async call => + { + var tool = _tools.Get(call.ToolName); + if (tool == null) + return (call, ToolResult.Fail($"알 수 없는 도구: {call.ToolName}"), 0L); + + var sw = System.Diagnostics.Stopwatch.StartNew(); + try + { + var input = call.ToolInput ?? JsonDocument.Parse("{}").RootElement; + var result = await tool.ExecuteAsync(input, context, ct); + sw.Stop(); + return (call, result, sw.ElapsedMilliseconds); + } + catch (Exception ex) + { + sw.Stop(); + return (call, ToolResult.Fail($"도구 실행 오류: {ex.Message}"), sw.ElapsedMilliseconds); + } + }).ToList(); + + var results = await Task.WhenAll(tasks); + + // 결과를 순서대로 메시지에 추가 + foreach (var (call, result, elapsed) in results) + { + EmitEvent( + result.Success ? AgentEventType.ToolResult : AgentEventType.Error, + call.ToolName, + TruncateOutput(result.Output, 200), + result.FilePath, + elapsedMs: elapsed, + iteration: iteration); + + if (result.Success) state.StatsSuccessCount++; else state.StatsFailCount++; + if (!statsUsedTools.Contains(call.ToolName)) + statsUsedTools.Add(call.ToolName); + + state.TotalToolCalls++; + messages.Add(LlmService.CreateToolResultMessage( + call.ToolId, call.ToolName, TruncateOutput(result.Output, Defaults.ToolResultTruncateLength))); + + if (!result.Success) + { + state.ConsecutiveErrors++; + if (state.ConsecutiveErrors > maxRetry) + { + messages.Add(LlmService.CreateToolResultMessage( + call.ToolId, call.ToolName, + $"[FAILED after retries] {TruncateOutput(result.Output, 500)}")); + } + } + else + { + state.ConsecutiveErrors = 0; + } + + // 감사 로그 + if (llm.EnableAuditLog) + { + AuditLogService.LogToolCall( + _conversationId, context.ActiveTab ?? "", + call.ToolName, + call.ToolInput?.ToString() ?? "", + TruncateOutput(result.Output, 500), + result.FilePath, result.Success); + } + } + } +} diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.cs b/src/AxCopilot/Services/Agent/AgentLoopService.cs index 739c646..9ffcd95 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopService.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopService.cs @@ -10,7 +10,7 @@ namespace AxCopilot.Services.Agent; /// 에이전트 루프 엔진: LLM과 대화하며 도구/스킬을 실행하는 반복 루프. /// 계획 → 도구 실행 → 관찰 → 재평가 패턴을 구현합니다. /// -public class AgentLoopService +public partial class AgentLoopService { private static App? CurrentApp => System.Windows.Application.Current as App; @@ -1331,493 +1331,4 @@ public class AgentLoopService } catch (Exception) { return call.ToolName; } } - - private static string TruncateOutput(string output, int maxLength) - { - if (output.Length <= maxLength) return output; - return output[..maxLength] + "\n... (출력 잘림)"; - } - - // ─── 병렬 도구 실행 ────────────────────────────────────────────────── - - // 읽기 전용 도구 (파일 상태를 변경하지 않음) - private static readonly HashSet ReadOnlyTools = new(StringComparer.OrdinalIgnoreCase) - { - "file_read", "glob", "grep_tool", "folder_map", "document_read", - "search_codebase", "code_search", "env_tool", "datetime_tool", - "dev_env_detect", "memory", "skill_manager", "json_tool", - "regex_tool", "base64_tool", "hash_tool", "image_analyze", - }; - - /// 도구 호출을 병렬 가능 / 순차 필수로 분류합니다. - private static (List Parallel, List Sequential) - ClassifyToolCalls(List calls) - { - var parallel = new List(); - var sequential = new List(); - - foreach (var call in calls) - { - if (ReadOnlyTools.Contains(call.ToolName ?? "")) - parallel.Add(call); - else - sequential.Add(call); - } - - // 읽기 전용 도구가 1개뿐이면 병렬화 의미 없음 - if (parallel.Count <= 1) - { - sequential.InsertRange(0, parallel); - parallel.Clear(); - } - - return (parallel, sequential); - } - - // ─── Phase 33-B: 도구 실행 루프 상태 (RunAsync에서 추출) ───────────── - /// 도구 실행 루프의 가변 상태. 메서드 간 공유. - private class ToolExecutionState - { - public int CurrentStep; - public int TotalToolCalls; - public int MaxIterations; - public int BaseMax; - public int MaxRetry; - public int ConsecutiveErrors; - public int StatsSuccessCount; - public int StatsFailCount; - public int StatsInputTokens; - public int StatsOutputTokens; - public List StatsUsedTools = new(); - public List PlanSteps = new(); - public bool DocumentPlanCalled; - public string? DocumentPlanPath; - public string? DocumentPlanTitle; - public string? DocumentPlanScaffold; - } - - /// 병렬 실행용 가변 상태 (ToolExecutionState의 서브셋). TODO: ToolExecutionState로 통합 예정. - private class ParallelState - { - public int CurrentStep; - public int TotalToolCalls; - public int MaxIterations; - public int ConsecutiveErrors; - public int StatsSuccessCount; - public int StatsFailCount; - public int StatsInputTokens; - public int StatsOutputTokens; - } - - /// - /// Phase 33-B: 단일 도구 호출을 처리합니다 (RunAsync에서 추출). - /// 훅 실행 → 도구 실행 → 결과 처리 → 검증까지 단일 도구 라이프사이클. - /// - /// LoopAction: Continue=다음 도구, Break=루프 중단, Return=값 반환. - private async Task<(ToolCallAction Action, string? ReturnValue)> ProcessSingleToolCallAsync( - LlmService.ContentBlock call, - List messages, - AgentContext context, - ToolExecutionState state, - List toolCalls, - string activeTabSnapshot, - int iteration, - CancellationToken ct) - { - var llm = _settings.Settings.Llm; - var tool = _tools.Get(call.ToolName); - if (tool == null) - { - var errResult = $"알 수 없는 도구: {call.ToolName}"; - EmitEvent(AgentEventType.Error, call.ToolName, errResult); - messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, errResult)); - return (ToolCallAction.Continue, null); - } - - // Task Decomposition: 단계 진행률 추적 - if (state.PlanSteps.Count > 0) - { - var summary = FormatToolCallSummary(call); - var newStep = TaskDecomposer.EstimateCurrentStep( - state.PlanSteps, call.ToolName, summary, state.CurrentStep); - if (newStep != state.CurrentStep) - { - state.CurrentStep = newStep; - EmitEvent(AgentEventType.StepStart, "", state.PlanSteps[state.CurrentStep], - stepCurrent: state.CurrentStep + 1, stepTotal: state.PlanSteps.Count); - } - } - - // 개발자 모드: 도구 호출 파라미터 상세 표시 - if (context.DevMode) - { - var paramJson = call.ToolInput?.ToString() ?? "{}"; - if (paramJson.Length > 500) paramJson = paramJson[..500] + "..."; - EmitEvent(AgentEventType.Thinking, call.ToolName, - $"[DEV] 도구 호출: {call.ToolName}\n파라미터: {paramJson}"); - } - - // 이벤트 로그 기록 - if (_eventLog != null) - _ = _eventLog.AppendAsync(AgentEventLogType.ToolRequest, - JsonSerializer.Serialize(new { toolName = call.ToolName, iteration })); - - EmitEvent(AgentEventType.ToolCall, call.ToolName, FormatToolCallSummary(call)); - - // 개발자 모드: 스텝 바이 스텝 승인 - if (context.DevModeStepApproval && UserDecisionCallback != null) - { - var decision = await UserDecisionCallback( - $"[DEV] 도구 '{call.ToolName}' 실행을 승인하시겠습니까?\n{FormatToolCallSummary(call)}", - new List { "승인", "건너뛰기", "중단" }); - if (decision == "중단") - { - EmitEvent(AgentEventType.Complete, "", "[DEV] 사용자가 실행을 중단했습니다"); - return (ToolCallAction.Return, "사용자가 개발자 모드에서 실행을 중단했습니다."); - } - if (decision == "건너뛰기") - { - messages.Add(LlmService.CreateToolResultMessage( - call.ToolId, call.ToolName, "[SKIPPED by developer] 사용자가 이 도구 실행을 건너뛰었습니다.")); - return (ToolCallAction.Continue, null); - } - } - - // 영향 범위 기반 의사결정 체크 - var decisionRequired = CheckDecisionRequired(call, context); - if (decisionRequired != null && UserDecisionCallback != null) - { - var decision = await UserDecisionCallback( - decisionRequired, - new List { "승인", "건너뛰기", "취소" }); - if (decision == "취소") - { - EmitEvent(AgentEventType.Complete, "", "사용자가 작업을 취소했습니다"); - return (ToolCallAction.Return, "사용자가 작업을 취소했습니다."); - } - if (decision == "건너뛰기") - { - messages.Add(LlmService.CreateToolResultMessage( - call.ToolId, call.ToolName, "[SKIPPED] 사용자가 이 작업을 건너뛰었습니다.")); - return (ToolCallAction.Continue, null); - } - } - - // ── Pre-Hook 실행 ── - await RunToolHooksAsync(llm, call, context, "pre", ct); - - // ── 도구 실행 ── - ToolResult result; - var sw = Stopwatch.StartNew(); - try - { - var input = call.ToolInput ?? JsonDocument.Parse("{}").RootElement; - result = await tool.ExecuteAsync(input, context, ct); - } - catch (OperationCanceledException) - { - EmitEvent(AgentEventType.Complete, "", "사용자가 작업을 취소했습니다."); - return (ToolCallAction.Return, "사용자가 작업을 취소했습니다."); - } - catch (Exception ex) - { - result = ToolResult.Fail($"도구 실행 오류: {ex.Message}"); - } - sw.Stop(); - - // ── Post-Hook 실행 ── - await RunToolHooksAsync(llm, call, context, "post", ct, result); - - // 개발자 모드: 도구 결과 상세 표시 - if (context.DevMode) - { - EmitEvent(AgentEventType.Thinking, call.ToolName, - $"[DEV] 결과: {(result.Success ? "성공" : "실패")}\n{TruncateOutput(result.Output, 500)}"); - } - - var tokenUsage = _llm.LastTokenUsage; - EmitEvent( - result.Success ? AgentEventType.ToolResult : AgentEventType.Error, - call.ToolName, - TruncateOutput(result.Output, 200), - result.FilePath, - elapsedMs: sw.ElapsedMilliseconds, - inputTokens: tokenUsage?.PromptTokens ?? 0, - outputTokens: tokenUsage?.CompletionTokens ?? 0, - toolInput: call.ToolInput?.ToString(), - iteration: iteration); - - // 통계 수집 - if (result.Success) state.StatsSuccessCount++; else state.StatsFailCount++; - state.StatsInputTokens += tokenUsage?.PromptTokens ?? 0; - state.StatsOutputTokens += tokenUsage?.CompletionTokens ?? 0; - if (!state.StatsUsedTools.Contains(call.ToolName)) - state.StatsUsedTools.Add(call.ToolName); - - // 감사 로그 기록 - if (llm.EnableAuditLog) - { - AuditLogService.LogToolCall( - _conversationId, activeTabSnapshot, - call.ToolName, - call.ToolInput.ToString() ?? "", - TruncateOutput(result.Output, 500), - result.FilePath, result.Success); - } - - // 이벤트 로그 - if (_eventLog != null) - _ = _eventLog.AppendAsync(AgentEventLogType.ToolResult, - JsonSerializer.Serialize(new - { - toolName = call.ToolName, - success = result.Success, - outputLength = result.Output?.Length ?? 0 - })); - - state.TotalToolCalls++; - - // 동적 반복 한도 확장 - if (state.TotalToolCalls > Defaults.ToolCallThresholdForExpansion && state.MaxIterations < state.BaseMax * 2) - state.MaxIterations = Math.Min(state.BaseMax * 2, 50); - - // 테스트-수정 루프 예산 확장 - if (call.ToolName == "test_loop" && (result.Output?.Contains("[AUTO_FIX:") ?? false)) - { - var testFixMax = llm.MaxTestFixIterations > 0 ? llm.MaxTestFixIterations : Defaults.MaxTestFixIterations; - var testFixBudget = state.BaseMax + testFixMax * 3; - if (state.MaxIterations < testFixBudget) - state.MaxIterations = Math.Min(testFixBudget, 60); - } - - // UI 스레드가 이벤트를 렌더링할 시간 확보 - await Task.Delay(80, ct); - - // Self-Reflection: 도구 실패 시 반성 프롬프트 추가 - if (!result.Success) - { - state.ConsecutiveErrors++; - if (state.ConsecutiveErrors <= state.MaxRetry) - { - var reflectionMsg = $"[Tool '{call.ToolName}' failed: {TruncateOutput(result.Output ?? "", 500)}]\n" + - $"Analyze why this failed. Consider: wrong parameters, wrong file path, missing prerequisites. " + - $"Try a different approach. (Error {state.ConsecutiveErrors}/{state.MaxRetry})"; - messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, reflectionMsg)); - EmitEvent(AgentEventType.Thinking, "", $"Self-Reflection: 실패 분석 후 재시도 ({state.ConsecutiveErrors}/{state.MaxRetry})"); - } - else - { - messages.Add(LlmService.CreateToolResultMessage( - call.ToolId, call.ToolName, - $"[FAILED after {state.MaxRetry} retries] {TruncateOutput(result.Output ?? "", 500)}\n" + - "Stop retrying this tool. Explain the error to the user and suggest alternative approaches.")); - - // 에이전트 메모리에 실패 원인 저장 - try - { - var memSvc = CurrentApp?.MemoryService; - if (memSvc != null && (_settings.Settings.Llm.EnableAgentMemory)) - { - memSvc.Add("correction", - $"도구 '{call.ToolName}' 반복 실패: {TruncateOutput(result.Output ?? "", Defaults.LogSummaryMaxLength)}", - $"conv:{_conversationId}", context.WorkFolder); - } - } - catch (Exception ex) { LogService.Warn($"[AgentLoop] 에이전트 메모리 저장 실패: {ex.Message}"); } - } - } - else - { - state.ConsecutiveErrors = 0; - - // ToolResultSizer 적용 - var sizedResult = ToolResultSizer.Apply(result.Output ?? "", call.ToolName); - messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, sizedResult.Output)); - - // document_plan 호출 플래그 + 메타데이터 저장 - if (call.ToolName == "document_plan" && result.Success) - { - state.DocumentPlanCalled = true; - try - { - var planJson = JsonDocument.Parse(result.Output ?? "{}"); - if (planJson.RootElement.TryGetProperty("path", out var p)) - state.DocumentPlanPath = p.GetString(); - if (planJson.RootElement.TryGetProperty("title", out var t)) - state.DocumentPlanTitle = t.GetString(); - if (planJson.RootElement.TryGetProperty("body", out var b)) - state.DocumentPlanScaffold = b.GetString(); - } - catch (Exception) { /* document_plan JSON 파싱 실패 — 선택적 메타데이터이므로 무시 */ } - - // html_create 즉시 호출 유도 - var toolHint = (result.Output?.Contains("document_assemble") ?? false) ? "document_assemble" : - (result.Output?.Contains("file_write") ?? false) ? "file_write" : "html_create"; - messages.Add(new ChatMessage { Role = "user", - Content = $"document_plan이 완료되었습니다. " + - $"위 결과의 body/sections의 [내용...] 부분을 실제 상세 내용으로 모두 채워서 " + - $"{toolHint} 도구를 지금 즉시 호출하세요. " + - $"각 섹션마다 반드시 충분한 내용을 작성하고, 설명 없이 도구를 바로 호출하세요." }); - EmitEvent(AgentEventType.Thinking, "", $"문서 개요 완성 — {toolHint} 호출 중..."); - } - - // 단일 문서 생성 도구 성공 → 즉시 루프 종료 - if (result.Success && IsTerminalDocumentTool(call.ToolName) && toolCalls.Count == 1) - { - var shouldVerify2 = activeTabSnapshot == "Code" - ? llm.Code.EnableCodeVerification && IsCodeVerificationTarget(call.ToolName) - : llm.EnableCoworkVerification && IsDocumentCreationTool(call.ToolName); - if (shouldVerify2) - { - await RunPostToolVerificationAsync(messages, call.ToolName, result, context, ct); - } - EmitEvent(AgentEventType.Complete, "", "에이전트 작업 완료"); - return (ToolCallAction.Return, result.Output ?? ""); - } - - // Post-Tool Verification: 탭별 검증 강제 - var shouldVerify = activeTabSnapshot == "Code" - ? llm.Code.EnableCodeVerification && IsCodeVerificationTarget(call.ToolName) - : llm.EnableCoworkVerification && IsDocumentCreationTool(call.ToolName); - if (shouldVerify && result.Success) - { - await RunPostToolVerificationAsync(messages, call.ToolName, result, context, ct); - } - } - - return (ToolCallAction.Continue, null); - } - - /// 도구 호출 처리 후 루프 제어 액션. - private enum ToolCallAction { Continue, Break, Return } - - /// Phase 33-B: 도구 훅(Pre/Post) 실행 헬퍼. - private async Task RunToolHooksAsync( - Models.LlmSettings llm, LlmService.ContentBlock call, - AgentContext context, string phase, CancellationToken ct, - ToolResult? result = null) - { - if (!llm.EnableToolHooks || llm.AgentHooks.Count == 0) return; - try - { - var hookResults = await AgentHookRunner.RunAsync( - llm.AgentHooks, call.ToolName, phase, - toolInput: call.ToolInput.ToString(), - toolOutput: result != null ? TruncateOutput(result.Output, 2048) : null, - success: result?.Success ?? false, - workFolder: context.WorkFolder, - timeoutMs: llm.ToolHookTimeoutMs, - ct: ct); - foreach (var pr in hookResults.Where(r => !r.Success)) - EmitEvent(AgentEventType.Error, call.ToolName, $"[Hook:{pr.HookName}] {pr.Output}"); - } - catch (Exception ex) { LogService.Warn($"[AgentLoop] {phase}ToolUse 훅 실패: {ex.Message}"); } - } - - /// DelegateAgentTool에서 호출하는 서브에이전트 실행기. - private async Task RunSubAgentAsync(string agentType, string task, string workFolder, CancellationToken ct) - { - try - { - var systemPrompt = $"당신은 전문 {agentType} 에이전트입니다. 주어진 작업을 수행하세요."; - var msgs = new List - { - new() { Role = "system", Content = systemPrompt }, - new() { Role = "user", Content = task } - }; - - // 서브에이전트는 간단한 텍스트 응답으로 처리 (도구 호출 없이) - var response = await _llm.SendAsync(msgs, ct); - return response; - } - catch (Exception ex) - { - return $"서브에이전트 실행 오류: {ex.Message}"; - } - } - - /// 읽기 전용 도구들을 병렬 실행합니다. - private async Task ExecuteToolsInParallelAsync( - List calls, - List messages, - AgentContext context, - List planSteps, - ParallelState state, - int baseMax, int maxRetry, - Models.LlmSettings llm, - int iteration, - CancellationToken ct, - List statsUsedTools) - { - EmitEvent(AgentEventType.Thinking, "", $"읽기 전용 도구 {calls.Count}개를 병렬 실행 중..."); - - var tasks = calls.Select(async call => - { - var tool = _tools.Get(call.ToolName); - if (tool == null) - return (call, ToolResult.Fail($"알 수 없는 도구: {call.ToolName}"), 0L); - - var sw = System.Diagnostics.Stopwatch.StartNew(); - try - { - var input = call.ToolInput ?? JsonDocument.Parse("{}").RootElement; - var result = await tool.ExecuteAsync(input, context, ct); - sw.Stop(); - return (call, result, sw.ElapsedMilliseconds); - } - catch (Exception ex) - { - sw.Stop(); - return (call, ToolResult.Fail($"도구 실행 오류: {ex.Message}"), sw.ElapsedMilliseconds); - } - }).ToList(); - - var results = await Task.WhenAll(tasks); - - // 결과를 순서대로 메시지에 추가 - foreach (var (call, result, elapsed) in results) - { - EmitEvent( - result.Success ? AgentEventType.ToolResult : AgentEventType.Error, - call.ToolName, - TruncateOutput(result.Output, 200), - result.FilePath, - elapsedMs: elapsed, - iteration: iteration); - - if (result.Success) state.StatsSuccessCount++; else state.StatsFailCount++; - if (!statsUsedTools.Contains(call.ToolName)) - statsUsedTools.Add(call.ToolName); - - state.TotalToolCalls++; - messages.Add(LlmService.CreateToolResultMessage( - call.ToolId, call.ToolName, TruncateOutput(result.Output, Defaults.ToolResultTruncateLength))); - - if (!result.Success) - { - state.ConsecutiveErrors++; - if (state.ConsecutiveErrors > maxRetry) - { - messages.Add(LlmService.CreateToolResultMessage( - call.ToolId, call.ToolName, - $"[FAILED after retries] {TruncateOutput(result.Output, 500)}")); - } - } - else - { - state.ConsecutiveErrors = 0; - } - - // 감사 로그 - if (llm.EnableAuditLog) - { - AuditLogService.LogToolCall( - _conversationId, context.ActiveTab ?? "", - call.ToolName, - call.ToolInput?.ToString() ?? "", - TruncateOutput(result.Output, 500), - result.FilePath, result.Success); - } - } - } } diff --git a/src/AxCopilot/ViewModels/SettingsViewModel.Methods.cs b/src/AxCopilot/ViewModels/SettingsViewModel.Methods.cs new file mode 100644 index 0000000..6f6cab2 --- /dev/null +++ b/src/AxCopilot/ViewModels/SettingsViewModel.Methods.cs @@ -0,0 +1,469 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.ComponentModel; +using System.Windows.Forms; +using AxCopilot.Models; +using AxCopilot.Services; +using AxCopilot.Themes; +using AxCopilot.Views; + +namespace AxCopilot.ViewModels; + +public partial class SettingsViewModel +{ + // ─── 인덱스 경로 메서드 ────────────────────────────────────────────── + public void AddIndexPath(string path) + { + var trimmed = path.Trim(); + if (string.IsNullOrWhiteSpace(trimmed)) return; + if (IndexPaths.Any(p => p.Equals(trimmed, StringComparison.OrdinalIgnoreCase))) return; + IndexPaths.Add(trimmed); + } + + public void RemoveIndexPath(string path) => IndexPaths.Remove(path); + + // ─── 인덱스 확장자 메서드 ────────────────────────────────────────── + public void AddExtension(string ext) + { + var trimmed = ext.Trim().ToLowerInvariant(); + if (string.IsNullOrWhiteSpace(trimmed)) return; + if (!trimmed.StartsWith(".")) trimmed = "." + trimmed; + if (IndexExtensions.Any(e => e.Equals(trimmed, StringComparison.OrdinalIgnoreCase))) return; + IndexExtensions.Add(trimmed); + } + + public void RemoveExtension(string ext) => IndexExtensions.Remove(ext); + + public void BrowseIndexPath() + { + using var dlg = new FolderBrowserDialog + { + Description = "인덱스할 폴더 선택", + UseDescriptionForTitle = true, + ShowNewFolderButton = false + }; + if (dlg.ShowDialog() != DialogResult.OK) return; + AddIndexPath(dlg.SelectedPath); + } + + // ─── 빠른 실행 단축키 메서드 ────────────────────────────────────────── + public bool AddShortcut() + { + if (string.IsNullOrWhiteSpace(_newKey) || string.IsNullOrWhiteSpace(_newTarget)) + return false; + + // 중복 키 확인 + if (AppShortcuts.Any(s => s.Key.Equals(_newKey.Trim(), StringComparison.OrdinalIgnoreCase))) + return false; + + AppShortcuts.Add(new AppShortcutModel + { + Key = _newKey.Trim(), + Description = _newDescription.Trim(), + Target = _newTarget.Trim(), + Type = _newType + }); + + NewKey = ""; NewDescription = ""; NewTarget = ""; NewType = "app"; + return true; + } + + public void RemoveShortcut(AppShortcutModel shortcut) => AppShortcuts.Remove(shortcut); + + /// 파일 선택 대화상자. 선택 시 NewTarget에 자동 설정. + public void BrowseTarget() + { + using var dlg = new OpenFileDialog + { + Filter = "실행 파일 (*.exe)|*.exe|모든 파일 (*.*)|*.*", + Title = "앱 선택" + }; + if (dlg.ShowDialog() != DialogResult.OK) return; + + NewTarget = dlg.FileName; + NewType = "app"; + if (string.IsNullOrWhiteSpace(NewDescription)) + NewDescription = System.IO.Path.GetFileNameWithoutExtension(dlg.FileName); + } + + public void SelectTheme(string key) + { + SelectedThemeKey = key; + ThemePreviewRequested?.Invoke(this, EventArgs.Empty); + } + + public void PickColor(ColorRowModel row) + { + using var dlg = new ColorDialog { FullOpen = true }; + try + { + var color = ThemeResourceHelper.HexColor(row.Hex); + dlg.Color = System.Drawing.Color.FromArgb(color.R, color.G, color.B); + } + catch (Exception) { /* 기본값 사용 */ } + + if (dlg.ShowDialog() != DialogResult.OK) return; + + row.Hex = $"#{dlg.Color.R:X2}{dlg.Color.G:X2}{dlg.Color.B:X2}"; + if (_selectedThemeKey == "custom") + ThemePreviewRequested?.Invoke(this, EventArgs.Empty); + } + + // 시스템 예약 프리픽스 (핸들러에서 이미 사용 중인 키) + private static readonly HashSet ReservedPrefixes = new(StringComparer.OrdinalIgnoreCase) + { + "=", "?", "#", "$", ";", "@", "~", ">", "!", + "emoji", "color", "recent", "note", "uninstall", "kill", "media", + "info", "*", "json", "encode", "port", "env", "snap", "help", + "pick", "date", "svc", "pipe", "journal", "routine", "batch", + "diff", "win", "stats", "fav", "rename", "monitor", "scaffold", + }; + + /// 설정 저장 전 프리픽스/키워드 충돌을 검사합니다. 충돌 시 메시지를 반환합니다. + public string? ValidateBeforeSave() + { + // 캡처 프리픽스 충돌 검사 + var cap = string.IsNullOrWhiteSpace(_capPrefix) ? "cap" : _capPrefix.Trim(); + if (cap != "cap" && ReservedPrefixes.Contains(cap)) + return $"캡처 프리픽스 '{cap}'은(는) 이미 사용 중인 예약어입니다."; + + // 빠른 실행 별칭 키 중복 검사 + var aliasKeys = AppShortcuts.Select(s => s.Key.ToLowerInvariant()).ToList(); + var duplicateAlias = aliasKeys.GroupBy(k => k).FirstOrDefault(g => g.Count() > 1); + if (duplicateAlias != null) + return $"빠른 실행 키워드 '{duplicateAlias.Key}'이(가) 중복되었습니다."; + + // 빠른 실행 별칭 키 vs 예약 프리픽스 충돌 + foreach (var key in aliasKeys) + { + if (ReservedPrefixes.Contains(key)) + return $"빠른 실행 키워드 '{key}'은(는) 시스템 예약어와 충돌합니다."; + } + + // 배치 명령 키 중복 검사 + var batchKeys = BatchCommands.Select(b => b.Key.ToLowerInvariant()).ToList(); + var duplicateBatch = batchKeys.GroupBy(k => k).FirstOrDefault(g => g.Count() > 1); + if (duplicateBatch != null) + return $"배치 명령 키워드 '{duplicateBatch.Key}'이(가) 중복되었습니다."; + + return null; // 문제 없음 + } + + public void Save() + { + // 충돌 검사 + var conflict = ValidateBeforeSave(); + if (conflict != null) + { + CustomMessageBox.Show( + conflict, + "AX Copilot — 설정 저장 오류", + System.Windows.MessageBoxButton.OK, + System.Windows.MessageBoxImage.Warning); + return; + } + + var s = _service.Settings; + s.Hotkey = _hotkey; + s.Launcher.MaxResults = _maxResults; + s.Launcher.Opacity = _opacity; + s.Launcher.Theme = _selectedThemeKey; + s.Launcher.Position = _launcherPosition; + s.Launcher.WebSearchEngine = _webSearchEngine; + s.Launcher.SnippetAutoExpand = _snippetAutoExpand; + s.Launcher.Language = _language; + L10n.SetLanguage(_language); + + // 기능 토글 저장 + s.Launcher.ShowNumberBadges = _showNumberBadges; + s.Launcher.EnableFavorites = _enableFavorites; + s.Launcher.EnableRecent = _enableRecent; + s.Launcher.EnableActionMode = _enableActionMode; + s.Launcher.CloseOnFocusLost = _closeOnFocusLost; + s.Launcher.ShowPrefixBadge = _showPrefixBadge; + s.Launcher.EnableIconAnimation = _enableIconAnimation; + s.Launcher.EnableRainbowGlow = _enableRainbowGlow; + s.Launcher.EnableSelectionGlow = _enableSelectionGlow; + s.Launcher.EnableRandomPlaceholder = _enableRandomPlaceholder; + s.Launcher.ShowLauncherBorder = _showLauncherBorder; + s.Launcher.ShortcutHelpUseThemeColor = _shortcutHelpUseThemeColor; + s.Launcher.EnableTextAction = _enableTextAction; + s.Launcher.EnableFileDialogIntegration = _enableFileDialogIntegration; + s.Launcher.EnableClipboardAutoCategory = _enableClipboardAutoCategory; + s.Launcher.MaxPinnedClipboardItems = _maxPinnedClipboardItems; + s.Launcher.TextActionTranslateLanguage = _textActionTranslateLanguage; + s.Llm.MaxSubAgents = _maxSubAgents; + s.Llm.PdfExportPath = _pdfExportPath; + s.Llm.TipDurationSeconds = _tipDurationSeconds; + + // LLM 공통 설정 저장 + s.Llm.Service = _llmService; + s.Llm.Streaming = _llmStreaming; + s.Llm.MaxContextTokens = _llmMaxContextTokens; + s.Llm.RetentionDays = _llmRetentionDays; + s.Llm.Temperature = _llmTemperature; + s.Llm.DefaultAgentPermission = _defaultAgentPermission; + s.Llm.DefaultOutputFormat = _defaultOutputFormat; + s.Llm.DefaultMood = _defaultMood; + s.Llm.AutoPreview = _autoPreview; + s.Llm.MaxAgentIterations = _maxAgentIterations; + s.Llm.MaxRetryOnError = _maxRetryOnError; + s.Llm.AgentLogLevel = _agentLogLevel; + s.Llm.AgentDecisionLevel = _agentDecisionLevel; + s.Llm.PlanMode = _planMode; + s.Llm.EnableMultiPassDocument = _enableMultiPassDocument; + s.Llm.EnableCoworkVerification = _enableCoworkVerification; + s.Llm.EnableFilePathHighlight = _enableFilePathHighlight; + s.Llm.FolderDataUsage = _folderDataUsage; + s.Llm.EnableAuditLog = _enableAuditLog; + s.Llm.EnableAgentMemory = _enableAgentMemory; + s.Llm.EnableProjectRules = _enableProjectRules; + s.Llm.MaxMemoryEntries = _maxMemoryEntries; + s.Llm.EnableImageInput = _enableImageInput; + s.Llm.MaxImageSizeKb = _maxImageSizeKb; + s.Llm.EnableToolHooks = _enableToolHooks; + s.Llm.ToolHookTimeoutMs = _toolHookTimeoutMs; + s.Llm.EnableSkillSystem = _enableSkillSystem; + s.Llm.SkillsFolderPath = _skillsFolderPath; + s.Llm.SlashPopupPageSize = _slashPopupPageSize; + s.Llm.EnableDragDropAiActions = _enableDragDropAiActions; + s.Llm.DragDropAutoSend = _dragDropAutoSend; + s.Llm.Code.EnableCodeReview = _enableCodeReview; + s.Llm.EnableAutoRouter = _enableAutoRouter; + s.Llm.AutoRouterConfidence = _autoRouterConfidence; + s.Llm.EnableChatRainbowGlow = _enableChatRainbowGlow; + s.Llm.NotifyOnComplete = _notifyOnComplete; + s.Llm.ShowTips = _showTips; + s.Llm.DevMode = _devMode; + s.Llm.DevModeStepApproval = _devModeStepApproval; + s.Llm.WorkflowVisualizer = _workflowVisualizer; + s.Llm.FreeTierMode = _freeTierMode; + s.Llm.FreeTierDelaySeconds = _freeTierDelaySeconds; + s.Llm.ShowTotalCallStats = _showTotalCallStats; + + // 서비스별 독립 설정 저장 + s.Llm.OllamaEndpoint = _ollamaEndpoint; + s.Llm.OllamaModel = _ollamaModel; + s.Llm.VllmEndpoint = _vllmEndpoint; + s.Llm.VllmModel = _vllmModel; + s.Llm.GeminiModel = _geminiModel; + s.Llm.ClaudeModel = _claudeModel; + s.Llm.GeminiApiKey = _geminiApiKey; + s.Llm.ClaudeApiKey = _claudeApiKey; + + // 내부 서비스 API 키 저장 (암호화 분기) + if (!string.IsNullOrEmpty(_ollamaApiKey) && _ollamaApiKey != "(저장됨)") + s.Llm.OllamaApiKey = CryptoService.EncryptIfEnabled(_ollamaApiKey, s.Llm.EncryptionEnabled); + if (!string.IsNullOrEmpty(_vllmApiKey) && _vllmApiKey != "(저장됨)") + s.Llm.VllmApiKey = CryptoService.EncryptIfEnabled(_vllmApiKey, s.Llm.EncryptionEnabled); + + // 활성 서비스의 설정을 기존 호환 필드에도 동기화 (LlmService.cs 호환) + switch (_llmService) + { + case "ollama": + s.Llm.Endpoint = _ollamaEndpoint; + s.Llm.Model = _ollamaModel; + s.Llm.EncryptedApiKey = s.Llm.OllamaApiKey; + break; + case "vllm": + s.Llm.Endpoint = _vllmEndpoint; + s.Llm.Model = _vllmModel; + s.Llm.EncryptedApiKey = s.Llm.VllmApiKey; + break; + case "gemini": + s.Llm.ApiKey = _geminiApiKey; + s.Llm.Model = _geminiModel; + break; + case "claude": + s.Llm.ApiKey = _claudeApiKey; + s.Llm.Model = _claudeModel; + break; + } + + // 등록 모델 저장 + s.Llm.RegisteredModels = RegisteredModels + .Where(rm => !string.IsNullOrWhiteSpace(rm.Alias)) + .Select(rm => new RegisteredModel + { + Alias = rm.Alias, + EncryptedModelName = rm.EncryptedModelName, + Service = rm.Service, + Endpoint = rm.Endpoint, + ApiKey = rm.ApiKey, + AuthType = rm.AuthType ?? "bearer", + Cp4dUrl = rm.Cp4dUrl ?? "", + Cp4dUsername = rm.Cp4dUsername ?? "", + Cp4dPassword = rm.Cp4dPassword ?? "", + }) + .ToList(); + + // 프롬프트 템플릿 저장 + s.Llm.PromptTemplates = PromptTemplates + .Where(pt => !string.IsNullOrWhiteSpace(pt.Name)) + .Select(pt => new PromptTemplate { Name = pt.Name, Content = pt.Content, Icon = pt.Icon }) + .ToList(); + + // 인덱스 경로 + 확장자 저장 + s.IndexPaths = IndexPaths.ToList(); + s.IndexExtensions = IndexExtensions.ToList(); + s.IndexSpeed = _indexSpeed; + + // 커스텀 색상 + 모양 저장 + var c = s.Launcher.CustomTheme ??= new CustomThemeColors(); + foreach (var row in ColorRows) + { + var prop = typeof(CustomThemeColors).GetProperty(row.Property); + prop?.SetValue(c, row.Hex); + } + c.WindowCornerRadius = _customWindowCornerRadius; + c.ItemCornerRadius = _customItemCornerRadius; + + // 빠른 실행 단축키 저장: + // batch/api/clipboard type은 그대로 유지, app/url/folder는 ViewModel 내용으로 교체 + var otherAliases = s.Aliases + .Where(a => a.Type is not ("app" or "url" or "folder" or "batch")) + .ToList(); + s.Aliases = otherAliases + .Concat(AppShortcuts.Select(sc => new AliasEntry + { + Key = sc.Key, + Type = sc.Type, + Target = sc.Target, + Description = string.IsNullOrWhiteSpace(sc.Description) ? null : sc.Description + })) + .Concat(BatchCommands.Select(b => new AliasEntry + { + Key = b.Key, + Type = "batch", + Target = b.Command, + ShowWindow = b.ShowWindow + })) + .ToList(); + + // 스니펫 저장 + s.Snippets = Snippets.Select(sn => new SnippetEntry + { + Key = sn.Key, + Name = sn.Name, + Content = sn.Content + }).ToList(); + + // 알림 설정 저장 + s.Reminder.Enabled = _reminderEnabled; + s.Reminder.Corner = _reminderCorner; + s.Reminder.IntervalMinutes = _reminderIntervalMinutes; + s.Reminder.DisplaySeconds = _reminderDisplaySeconds; + + // 캡처 설정 저장 + s.ScreenCapture.Prefix = string.IsNullOrWhiteSpace(_capPrefix) ? "cap" : _capPrefix.Trim(); + s.ScreenCapture.GlobalHotkeyEnabled = _capGlobalHotkeyEnabled; + s.ScreenCapture.GlobalHotkey = string.IsNullOrWhiteSpace(_capGlobalHotkey) ? "PrintScreen" : _capGlobalHotkey.Trim(); + s.ScreenCapture.GlobalHotkeyMode = _capGlobalMode; + s.ScreenCapture.ScrollDelayMs = Math.Max(50, _capScrollDelayMs); + + // 클립보드 히스토리 설정 저장 + s.ClipboardHistory.Enabled = _clipboardEnabled; + s.ClipboardHistory.MaxItems = _clipboardMaxItems; + + // 시스템 명령 설정 저장 + var sc = s.SystemCommands; + sc.ShowLock = _sysShowLock; + sc.ShowSleep = _sysShowSleep; + sc.ShowRestart = _sysShowRestart; + sc.ShowShutdown = _sysShowShutdown; + sc.ShowHibernate = _sysShowHibernate; + sc.ShowLogout = _sysShowLogout; + sc.ShowRecycleBin = _sysShowRecycleBin; + + // 시스템 명령 별칭 저장 + var cmdAliases = new Dictionary>(); + void SaveAlias(string key, string val) + { + var list = ParseAliases(val); + if (list.Count > 0) cmdAliases[key] = list; + } + SaveAlias("lock", _aliasLock); + SaveAlias("sleep", _aliasSleep); + SaveAlias("restart", _aliasRestart); + SaveAlias("shutdown", _aliasShutdown); + SaveAlias("hibernate", _aliasHibernate); + SaveAlias("logout", _aliasLogout); + SaveAlias("recycle", _aliasRecycle); + sc.CommandAliases = cmdAliases; + + _service.Save(); + SaveCompleted?.Invoke(this, EventArgs.Empty); + } + + public bool AddBatchCommand() + { + if (string.IsNullOrWhiteSpace(_newBatchKey) || string.IsNullOrWhiteSpace(_newBatchCommand)) + return false; + if (BatchCommands.Any(b => b.Key.Equals(_newBatchKey.Trim(), StringComparison.OrdinalIgnoreCase))) + return false; + + BatchCommands.Add(new BatchCommandModel + { + Key = _newBatchKey.Trim(), + Command = _newBatchCommand.Trim(), + ShowWindow = _newBatchShowWindow + }); + NewBatchKey = ""; NewBatchCommand = ""; NewBatchShowWindow = false; + return true; + } + + public void RemoveBatchCommand(BatchCommandModel cmd) => BatchCommands.Remove(cmd); + + // ─── 스니펫 메서드 ────────────────────────────────────────────────────── + public bool AddSnippet() + { + if (string.IsNullOrWhiteSpace(_newSnippetKey) || string.IsNullOrWhiteSpace(_newSnippetContent)) + return false; + if (Snippets.Any(sn => sn.Key.Equals(_newSnippetKey.Trim(), StringComparison.OrdinalIgnoreCase))) + return false; + + Snippets.Add(new SnippetRowModel + { + Key = _newSnippetKey.Trim(), + Name = _newSnippetName.Trim(), + Content = _newSnippetContent.Trim() + }); + NewSnippetKey = ""; NewSnippetName = ""; NewSnippetContent = ""; + return true; + } + + public void RemoveSnippet(SnippetRowModel row) => Snippets.Remove(row); + + // ─── 캡처 메서드 ──────────────────────────────────────────────────────── + public void ResetCapPrefix() => CapPrefix = "cap"; + + public void ResetCapGlobalHotkey() + { + CapGlobalHotkey = "PrintScreen"; + CapGlobalMode = "screen"; + } + + // ─── 알림 메서드 ──────────────────────────────────────────────────────── + public List GetReminderCategories() => _service.Settings.Reminder.EnabledCategories; + + // ─── 시스템 명령 메서드 ───────────────────────────────────────────────── + public void ResetSystemCommandAliases() + { + AliasLock = ""; AliasSleep = ""; AliasRestart = ""; + AliasShutdown = ""; AliasHibernate = ""; AliasLogout = ""; AliasRecycle = ""; + } + + private static string FormatAliases(Dictionary> dict, string key) + => dict.TryGetValue(key, out var list) ? string.Join(", ", list) : ""; + + private static List ParseAliases(string input) + => input.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .ToList(); + + public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string? n = null) + => PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(n)); +} diff --git a/src/AxCopilot/ViewModels/SettingsViewModel.Properties.cs b/src/AxCopilot/ViewModels/SettingsViewModel.Properties.cs new file mode 100644 index 0000000..7ef438b --- /dev/null +++ b/src/AxCopilot/ViewModels/SettingsViewModel.Properties.cs @@ -0,0 +1,837 @@ +using System.Collections.ObjectModel; +using AxCopilot.Models; + +namespace AxCopilot.ViewModels; + +public partial class SettingsViewModel +{ + /// CodeSettings 바인딩용 프로퍼티. XAML에서 {Binding Code.EnableLsp} 등으로 접근. + public Models.CodeSettings Code => _service.Settings.Llm.Code; + + // ─── 등록 모델 목록 ─────────────────────────────────────────────────── + public ObservableCollection RegisteredModels { get; } = new(); + + public string LlmService + { + get => _llmService; + set { _llmService = value; OnPropertyChanged(); OnPropertyChanged(nameof(IsInternalService)); OnPropertyChanged(nameof(IsExternalService)); OnPropertyChanged(nameof(NeedsEndpoint)); OnPropertyChanged(nameof(NeedsApiKey)); OnPropertyChanged(nameof(IsGeminiSelected)); OnPropertyChanged(nameof(IsClaudeSelected)); } + } + public bool IsInternalService => _llmService is "ollama" or "vllm"; + public bool IsExternalService => _llmService is "gemini" or "claude"; + public bool NeedsEndpoint => _llmService is "ollama" or "vllm"; + public bool NeedsApiKey => _llmService is not "ollama"; + public bool IsGeminiSelected => _llmService == "gemini"; + public bool IsClaudeSelected => _llmService == "claude"; + + // ── Ollama 설정 ── + public string OllamaEndpoint { get => _ollamaEndpoint; set { _ollamaEndpoint = value; OnPropertyChanged(); } } + public string OllamaApiKey { get => _ollamaApiKey; set { _ollamaApiKey = value; OnPropertyChanged(); } } + public string OllamaModel { get => _ollamaModel; set { _ollamaModel = value; OnPropertyChanged(); } } + + // ── vLLM 설정 ── + public string VllmEndpoint { get => _vllmEndpoint; set { _vllmEndpoint = value; OnPropertyChanged(); } } + public string VllmApiKey { get => _vllmApiKey; set { _vllmApiKey = value; OnPropertyChanged(); } } + public string VllmModel { get => _vllmModel; set { _vllmModel = value; OnPropertyChanged(); } } + + // ── Gemini 설정 ── + public string GeminiApiKey { get => _geminiApiKey; set { _geminiApiKey = value; OnPropertyChanged(); } } + public string GeminiModel { get => _geminiModel; set { _geminiModel = value; OnPropertyChanged(); } } + + // ── Claude 설정 ── + public string ClaudeApiKey { get => _claudeApiKey; set { _claudeApiKey = value; OnPropertyChanged(); } } + public string ClaudeModel { get => _claudeModel; set { _claudeModel = value; OnPropertyChanged(); } } + + // ── 공통 응답 설정 ── + public bool LlmStreaming + { + get => _llmStreaming; + set { _llmStreaming = value; OnPropertyChanged(); } + } + public int LlmMaxContextTokens + { + get => _llmMaxContextTokens; + set { _llmMaxContextTokens = value; OnPropertyChanged(); } + } + public int LlmRetentionDays + { + get => _llmRetentionDays; + set { _llmRetentionDays = value; OnPropertyChanged(); } + } + public double LlmTemperature + { + get => _llmTemperature; + set { _llmTemperature = Math.Round(Math.Clamp(value, 0.0, 2.0), 1); OnPropertyChanged(); } + } + + // 에이전트 기본 파일 접근 권한 + private string _defaultAgentPermission; + public string DefaultAgentPermission + { + get => _defaultAgentPermission; + set { _defaultAgentPermission = value; OnPropertyChanged(); } + } + + // ── 코워크/에이전트 고급 설정 ── + private string _defaultOutputFormat; + public string DefaultOutputFormat + { + get => _defaultOutputFormat; + set { _defaultOutputFormat = value; OnPropertyChanged(); } + } + + private string _autoPreview; + public string AutoPreview + { + get => _autoPreview; + set { _autoPreview = value; OnPropertyChanged(); } + } + + private int _maxAgentIterations; + public int MaxAgentIterations + { + get => _maxAgentIterations; + set { _maxAgentIterations = Math.Clamp(value, 1, 100); OnPropertyChanged(); } + } + + private int _maxRetryOnError; + public int MaxRetryOnError + { + get => _maxRetryOnError; + set { _maxRetryOnError = Math.Clamp(value, 0, 10); OnPropertyChanged(); } + } + + private string _agentLogLevel; + public string AgentLogLevel + { + get => _agentLogLevel; + set { _agentLogLevel = value; OnPropertyChanged(); } + } + + private string _agentDecisionLevel = "detailed"; + public string AgentDecisionLevel + { + get => _agentDecisionLevel; + set { _agentDecisionLevel = value; OnPropertyChanged(); } + } + + private string _planMode = "off"; + public string PlanMode + { + get => _planMode; + set { _planMode = value; OnPropertyChanged(); } + } + + private bool _enableMultiPassDocument; + public bool EnableMultiPassDocument + { + get => _enableMultiPassDocument; + set { _enableMultiPassDocument = value; OnPropertyChanged(); } + } + + private bool _enableCoworkVerification; + public bool EnableCoworkVerification + { + get => _enableCoworkVerification; + set { _enableCoworkVerification = value; OnPropertyChanged(); } + } + + private bool _enableFilePathHighlight = true; + public bool EnableFilePathHighlight + { + get => _enableFilePathHighlight; + set { _enableFilePathHighlight = value; OnPropertyChanged(); } + } + + private string _folderDataUsage; + public string FolderDataUsage + { + get => _folderDataUsage; + set { _folderDataUsage = value; OnPropertyChanged(); } + } + + // ── 모델 폴백 + 보안 + MCP ── + private bool _enableAuditLog; + public bool EnableAuditLog + { + get => _enableAuditLog; + set { _enableAuditLog = value; OnPropertyChanged(); } + } + + private bool _enableAgentMemory; + public bool EnableAgentMemory + { + get => _enableAgentMemory; + set { _enableAgentMemory = value; OnPropertyChanged(); } + } + + private bool _enableProjectRules = true; + public bool EnableProjectRules + { + get => _enableProjectRules; + set { _enableProjectRules = value; OnPropertyChanged(); } + } + + private int _maxMemoryEntries; + public int MaxMemoryEntries + { + get => _maxMemoryEntries; + set { _maxMemoryEntries = value; OnPropertyChanged(); } + } + + // ── 이미지 입력 (멀티모달) ── + private bool _enableImageInput = true; + public bool EnableImageInput + { + get => _enableImageInput; + set { _enableImageInput = value; OnPropertyChanged(); } + } + + private int _maxImageSizeKb = 5120; + public int MaxImageSizeKb + { + get => _maxImageSizeKb; + set { _maxImageSizeKb = value; OnPropertyChanged(); } + } + + // ── 자동 모델 라우팅 ── + private bool _enableAutoRouter; + public bool EnableAutoRouter + { + get => _enableAutoRouter; + set { _enableAutoRouter = value; OnPropertyChanged(); } + } + + private double _autoRouterConfidence = 0.7; + public double AutoRouterConfidence + { + get => _autoRouterConfidence; + set { _autoRouterConfidence = value; OnPropertyChanged(); } + } + + // ── 에이전트 훅 시스템 ── + private bool _enableToolHooks = true; + public bool EnableToolHooks + { + get => _enableToolHooks; + set { _enableToolHooks = value; OnPropertyChanged(); } + } + + private int _toolHookTimeoutMs = 10000; + public int ToolHookTimeoutMs + { + get => _toolHookTimeoutMs; + set { _toolHookTimeoutMs = value; OnPropertyChanged(); } + } + + // ── 스킬 시스템 ── + private bool _enableSkillSystem = true; + public bool EnableSkillSystem + { + get => _enableSkillSystem; + set { _enableSkillSystem = value; OnPropertyChanged(); } + } + + private string _skillsFolderPath = ""; + public string SkillsFolderPath + { + get => _skillsFolderPath; + set { _skillsFolderPath = value; OnPropertyChanged(); } + } + + private int _slashPopupPageSize = 6; + public int SlashPopupPageSize + { + get => _slashPopupPageSize; + set { _slashPopupPageSize = Math.Clamp(value, 3, 10); OnPropertyChanged(); } + } + + // ── 드래그&드롭 AI ── + private bool _enableDragDropAiActions = true; + public bool EnableDragDropAiActions + { + get => _enableDragDropAiActions; + set { _enableDragDropAiActions = value; OnPropertyChanged(); } + } + + private bool _dragDropAutoSend; + public bool DragDropAutoSend + { + get => _dragDropAutoSend; + set { _dragDropAutoSend = value; OnPropertyChanged(); } + } + + // ── 코드 리뷰 ── + private bool _enableCodeReview = true; + public bool EnableCodeReview + { + get => _enableCodeReview; + set { _enableCodeReview = value; OnPropertyChanged(); } + } + + // ── 시각 효과 + 알림 + 개발자 모드 (공통) ── + private bool _enableChatRainbowGlow; + public bool EnableChatRainbowGlow + { + get => _enableChatRainbowGlow; + set { _enableChatRainbowGlow = value; OnPropertyChanged(); } + } + + private bool _notifyOnComplete; + public bool NotifyOnComplete + { + get => _notifyOnComplete; + set { _notifyOnComplete = value; OnPropertyChanged(); } + } + + private bool _showTips; + public bool ShowTips + { + get => _showTips; + set { _showTips = value; OnPropertyChanged(); } + } + + private bool _devMode; + public bool DevMode + { + get => _devMode; + set { _devMode = value; OnPropertyChanged(); } + } + + private bool _devModeStepApproval; + public bool DevModeStepApproval + { + get => _devModeStepApproval; + set { _devModeStepApproval = value; OnPropertyChanged(); } + } + + private bool _workflowVisualizer; + public bool WorkflowVisualizer + { + get => _workflowVisualizer; + set { _workflowVisualizer = value; OnPropertyChanged(); } + } + + private bool _freeTierMode; + public bool FreeTierMode + { + get => _freeTierMode; + set { _freeTierMode = value; OnPropertyChanged(); } + } + + private int _freeTierDelaySeconds = 4; + public int FreeTierDelaySeconds + { + get => _freeTierDelaySeconds; + set { _freeTierDelaySeconds = value; OnPropertyChanged(); } + } + + private bool _showTotalCallStats; + public bool ShowTotalCallStats + { + get => _showTotalCallStats; + set { _showTotalCallStats = value; OnPropertyChanged(); } + } + + private string _defaultMood = "modern"; + public string DefaultMood + { + get => _defaultMood; + set { _defaultMood = value; OnPropertyChanged(); } + } + + // 차단 경로/확장자 (읽기 전용 UI) + public ObservableCollection BlockedPaths { get; } = new(); + public ObservableCollection BlockedExtensions { get; } = new(); + + public string Hotkey + { + get => _hotkey; + set { _hotkey = value; OnPropertyChanged(); } + } + + public int MaxResults + { + get => _maxResults; + set { _maxResults = value; OnPropertyChanged(); } + } + + public double Opacity + { + get => _opacity; + set { _opacity = value; OnPropertyChanged(); OnPropertyChanged(nameof(OpacityPercent)); } + } + + public int OpacityPercent => (int)Math.Round(_opacity * 100); + + public string SelectedThemeKey + { + get => _selectedThemeKey; + set + { + _selectedThemeKey = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(IsCustomTheme)); + foreach (var card in ThemeCards) + card.IsSelected = card.Key == value; + } + } + + public bool IsCustomTheme => _selectedThemeKey == "custom"; + + public string LauncherPosition + { + get => _launcherPosition; + set { _launcherPosition = value; OnPropertyChanged(); } + } + + public string WebSearchEngine + { + get => _webSearchEngine; + set { _webSearchEngine = value; OnPropertyChanged(); } + } + + public bool SnippetAutoExpand + { + get => _snippetAutoExpand; + set { _snippetAutoExpand = value; OnPropertyChanged(); } + } + + public string Language + { + get => _language; + set { _language = value; OnPropertyChanged(); } + } + + public string IndexSpeed + { + get => _indexSpeed; + set { _indexSpeed = value; OnPropertyChanged(); OnPropertyChanged(nameof(IndexSpeedHint)); } + } + + public string IndexSpeedHint => _indexSpeed switch + { + "fast" => "CPU 사용률이 높아질 수 있습니다. 고성능 PC에 권장합니다.", + "slow" => "인덱싱이 오래 걸리지만 PC 성능에 영향을 주지 않습니다.", + _ => "일반적인 PC에 적합한 균형 설정입니다.", + }; + + // ─── 기능 토글 속성 ─────────────────────────────────────────────────── + + public bool ShowNumberBadges + { + get => _showNumberBadges; + set { _showNumberBadges = value; OnPropertyChanged(); } + } + + public bool EnableFavorites + { + get => _enableFavorites; + set { _enableFavorites = value; OnPropertyChanged(); } + } + + public bool EnableRecent + { + get => _enableRecent; + set { _enableRecent = value; OnPropertyChanged(); } + } + + public bool EnableActionMode + { + get => _enableActionMode; + set { _enableActionMode = value; OnPropertyChanged(); } + } + + public bool CloseOnFocusLost + { + get => _closeOnFocusLost; + set { _closeOnFocusLost = value; OnPropertyChanged(); } + } + + public bool ShowPrefixBadge + { + get => _showPrefixBadge; + set { _showPrefixBadge = value; OnPropertyChanged(); } + } + + public bool EnableIconAnimation + { + get => _enableIconAnimation; + set { _enableIconAnimation = value; OnPropertyChanged(); } + } + + public bool EnableRainbowGlow + { + get => _enableRainbowGlow; + set { _enableRainbowGlow = value; OnPropertyChanged(); } + } + + public bool EnableSelectionGlow + { + get => _enableSelectionGlow; + set { _enableSelectionGlow = value; OnPropertyChanged(); } + } + + public bool EnableRandomPlaceholder + { + get => _enableRandomPlaceholder; + set { _enableRandomPlaceholder = value; OnPropertyChanged(); } + } + + public bool ShowLauncherBorder + { + get => _showLauncherBorder; + set { _showLauncherBorder = value; OnPropertyChanged(); } + } + + // ─── v1.4.0 신기능 설정 ────────────────────────────────────────────────── + + private bool _enableTextAction = true; + public bool EnableTextAction + { + get => _enableTextAction; + set { _enableTextAction = value; OnPropertyChanged(); } + } + + private bool _enableFileDialogIntegration = false; + public bool EnableFileDialogIntegration + { + get => _enableFileDialogIntegration; + set { _enableFileDialogIntegration = value; OnPropertyChanged(); } + } + + private bool _enableClipboardAutoCategory = true; + public bool EnableClipboardAutoCategory + { + get => _enableClipboardAutoCategory; + set { _enableClipboardAutoCategory = value; OnPropertyChanged(); } + } + + private int _maxPinnedClipboardItems = 20; + public int MaxPinnedClipboardItems + { + get => _maxPinnedClipboardItems; + set { _maxPinnedClipboardItems = value; OnPropertyChanged(); } + } + + private string _textActionTranslateLanguage = "auto"; + public string TextActionTranslateLanguage + { + get => _textActionTranslateLanguage; + set { _textActionTranslateLanguage = value; OnPropertyChanged(); } + } + + private int _maxSubAgents = 3; + public int MaxSubAgents + { + get => _maxSubAgents; + set { _maxSubAgents = value; OnPropertyChanged(); } + } + + private string _pdfExportPath = ""; + public string PdfExportPath + { + get => _pdfExportPath; + set { _pdfExportPath = value; OnPropertyChanged(); } + } + + private int _tipDurationSeconds = 5; + public int TipDurationSeconds + { + get => _tipDurationSeconds; + set { _tipDurationSeconds = value; OnPropertyChanged(); } + } + + public bool ShortcutHelpUseThemeColor + { + get => _shortcutHelpUseThemeColor; + set { _shortcutHelpUseThemeColor = value; OnPropertyChanged(); } + } + + // ─── 테마 카드 목록 ─────────────────────────────────────────────────── + public List ThemeCards { get; } = new() + { + new() + { + Key = "system", Name = "시스템", + PreviewBackground = "#1E1E1E", PreviewText = "#FFFFFF", + PreviewSubText = "#888888", PreviewAccent = "#0078D4", + PreviewItem = "#2D2D2D", PreviewBorder = "#3D3D3D" + }, + new() + { + Key = "dark", Name = "Dark", + PreviewBackground = "#1A1B2E", PreviewText = "#F0F0FF", + PreviewSubText = "#7A7D9C", PreviewAccent = "#4B5EFC", + PreviewItem = "#252637", PreviewBorder = "#2E2F4A" + }, + new() + { + Key = "light", Name = "Light", + PreviewBackground = "#FAFAFA", PreviewText = "#1A1B2E", + PreviewSubText = "#666680", PreviewAccent = "#4B5EFC", + PreviewItem = "#F0F0F8", PreviewBorder = "#E0E0F0" + }, + new() + { + Key = "oled", Name = "OLED", + PreviewBackground = "#000000", PreviewText = "#FFFFFF", + PreviewSubText = "#888899", PreviewAccent = "#5C6EFF", + PreviewItem = "#0A0A14", PreviewBorder = "#1A1A2E" + }, + new() + { + Key = "nord", Name = "Nord", + PreviewBackground = "#2E3440", PreviewText = "#ECEFF4", + PreviewSubText = "#D8DEE9", PreviewAccent = "#88C0D0", + PreviewItem = "#3B4252", PreviewBorder = "#434C5E" + }, + new() + { + Key = "monokai", Name = "Monokai", + PreviewBackground = "#272822", PreviewText = "#F8F8F2", + PreviewSubText = "#75715E", PreviewAccent = "#A6E22E", + PreviewItem = "#3E3D32", PreviewBorder = "#49483E" + }, + new() + { + Key = "catppuccin", Name = "Catppuccin", + PreviewBackground = "#1E1E2E", PreviewText = "#CDD6F4", + PreviewSubText = "#A6ADC8", PreviewAccent = "#CBA6F7", + PreviewItem = "#313244", PreviewBorder = "#45475A" + }, + new() + { + Key = "sepia", Name = "Sepia", + PreviewBackground = "#F5EFE0", PreviewText = "#3C2F1A", + PreviewSubText = "#7A6040", PreviewAccent = "#C0822A", + PreviewItem = "#EDE6D6", PreviewBorder = "#D8CCBA" + }, + new() + { + Key = "alfred", Name = "Indigo", + PreviewBackground = "#26273B", PreviewText = "#EEEEFF", + PreviewSubText = "#8888BB", PreviewAccent = "#8877EE", + PreviewItem = "#3B3D60", PreviewBorder = "#40416A" + }, + new() + { + Key = "alfredlight", Name = "Frost", + PreviewBackground = "#FFFFFF", PreviewText = "#1A1A2E", + PreviewSubText = "#9090AA", PreviewAccent = "#5555EE", + PreviewItem = "#E8E9FF", PreviewBorder = "#DCDCEE" + }, + new() + { + Key = "codex", Name = "Codex", + PreviewBackground = "#FFFFFF", PreviewText = "#111111", + PreviewSubText = "#6B7280", PreviewAccent = "#7C3AED", + PreviewItem = "#F5F5F7", PreviewBorder = "#E5E7EB" + }, + new() + { + Key = "custom", Name = "커스텀", + PreviewBackground = "#1A1B2E", PreviewText = "#F0F0FF", + PreviewSubText = "#7A7D9C", PreviewAccent = "#4B5EFC", + PreviewItem = "#252637", PreviewBorder = "#2E2F4A" + }, + }; + + // ─── 커스텀 색상 행 목록 ────────────────────────────────────────────── + public List ColorRows { get; } + + // ─── 프롬프트 템플릿 ────────────────────────────────────────────────── + public ObservableCollection PromptTemplates { get; } = new(); + + // ─── 배치 명령 ──────────────────────────────────────────────────────── + public ObservableCollection BatchCommands { get; } = new(); + + private string _newBatchKey = ""; + private string _newBatchCommand = ""; + private bool _newBatchShowWindow; + + public string NewBatchKey + { + get => _newBatchKey; + set { _newBatchKey = value; OnPropertyChanged(); } + } + public string NewBatchCommand + { + get => _newBatchCommand; + set { _newBatchCommand = value; OnPropertyChanged(); } + } + public bool NewBatchShowWindow + { + get => _newBatchShowWindow; + set { _newBatchShowWindow = value; OnPropertyChanged(); } + } + + // ─── 빠른 실행 단축키 ───────────────────────────────────────────────── + public ObservableCollection AppShortcuts { get; } = new(); + + private string _newKey = ""; + private string _newDescription = ""; + private string _newTarget = ""; + private string _newType = "app"; + + public string NewKey { get => _newKey; set { _newKey = value; OnPropertyChanged(); } } + public string NewDescription { get => _newDescription; set { _newDescription = value; OnPropertyChanged(); } } + public string NewTarget { get => _newTarget; set { _newTarget = value; OnPropertyChanged(); } } + public string NewType { get => _newType; set { _newType = value; OnPropertyChanged(); } } + + // ─── 커스텀 테마 모서리 라운딩 ─────────────────────────────────────────── + private int _customWindowCornerRadius = 20; + private int _customItemCornerRadius = 10; + + public int CustomWindowCornerRadius + { + get => _customWindowCornerRadius; + set { _customWindowCornerRadius = Math.Clamp(value, 0, 30); OnPropertyChanged(); } + } + + public int CustomItemCornerRadius + { + get => _customItemCornerRadius; + set { _customItemCornerRadius = Math.Clamp(value, 0, 20); OnPropertyChanged(); } + } + + // ─── 인덱스 경로 ────────────────────────────────────────────────────── + public ObservableCollection IndexPaths { get; } = new(); + + // ─── 인덱스 확장자 ────────────────────────────────────────────────── + public ObservableCollection IndexExtensions { get; } = new(); + + // ─── 스니펫 설정 ────────────────────────────────────────────────────────── + public ObservableCollection Snippets { get; } = new(); + + private string _newSnippetKey = ""; + private string _newSnippetName = ""; + private string _newSnippetContent = ""; + + public string NewSnippetKey { get => _newSnippetKey; set { _newSnippetKey = value; OnPropertyChanged(); } } + public string NewSnippetName { get => _newSnippetName; set { _newSnippetName = value; OnPropertyChanged(); } } + public string NewSnippetContent { get => _newSnippetContent; set { _newSnippetContent = value; OnPropertyChanged(); } } + + // ─── 클립보드 히스토리 설정 ──────────────────────────────────────────────── + private bool _clipboardEnabled; + private int _clipboardMaxItems; + + public bool ClipboardEnabled + { + get => _clipboardEnabled; + set { _clipboardEnabled = value; OnPropertyChanged(); } + } + + public int ClipboardMaxItems + { + get => _clipboardMaxItems; + set { _clipboardMaxItems = value; OnPropertyChanged(); } + } + + // ─── 스크린 캡처 설정 ────────────────────────────────────────────────────── + private string _capPrefix = "cap"; + private bool _capGlobalHotkeyEnabled; + private string _capGlobalHotkey = "PrintScreen"; + private string _capGlobalMode = "screen"; + private int _capScrollDelayMs = 120; + + public string CapPrefix + { + get => _capPrefix; + set { _capPrefix = value; OnPropertyChanged(); } + } + + public bool CapGlobalHotkeyEnabled + { + get => _capGlobalHotkeyEnabled; + set { _capGlobalHotkeyEnabled = value; OnPropertyChanged(); } + } + + public string CapGlobalHotkey + { + get => _capGlobalHotkey; + set { _capGlobalHotkey = value; OnPropertyChanged(); } + } + + public string CapGlobalMode + { + get => _capGlobalMode; + set { _capGlobalMode = value; OnPropertyChanged(); } + } + + public int CapScrollDelayMs + { + get => _capScrollDelayMs; + set { _capScrollDelayMs = value; OnPropertyChanged(); OnPropertyChanged(nameof(CapScrollDelayMsStr)); } + } + + /// ComboBox SelectedValue와 string 바인딩 용도 + public string CapScrollDelayMsStr + { + get => _capScrollDelayMs.ToString(); + set { if (int.TryParse(value, out int v)) CapScrollDelayMs = v; } + } + + // ─── 잠금 해제 알림 설정 ────────────────────────────────────────────────── + private bool _reminderEnabled; + private string _reminderCorner = "bottom-right"; + private int _reminderIntervalMinutes = 60; + private int _reminderDisplaySeconds = 15; + + public bool ReminderEnabled + { + get => _reminderEnabled; + set { _reminderEnabled = value; OnPropertyChanged(); } + } + + public string ReminderCorner + { + get => _reminderCorner; + set { _reminderCorner = value; OnPropertyChanged(); } + } + + public int ReminderIntervalMinutes + { + get => _reminderIntervalMinutes; + set { _reminderIntervalMinutes = value; OnPropertyChanged(); } + } + + public int ReminderDisplaySeconds + { + get => _reminderDisplaySeconds; + set { _reminderDisplaySeconds = value; OnPropertyChanged(); } + } + + // ─── 시스템 명령 설정 ────────────────────────────────────────────────────── + private bool _sysShowLock; + private bool _sysShowSleep; + private bool _sysShowRestart; + private bool _sysShowShutdown; + private bool _sysShowHibernate; + private bool _sysShowLogout; + private bool _sysShowRecycleBin; + + public bool SysShowLock { get => _sysShowLock; set { _sysShowLock = value; OnPropertyChanged(); } } + public bool SysShowSleep { get => _sysShowSleep; set { _sysShowSleep = value; OnPropertyChanged(); } } + public bool SysShowRestart { get => _sysShowRestart; set { _sysShowRestart = value; OnPropertyChanged(); } } + public bool SysShowShutdown { get => _sysShowShutdown; set { _sysShowShutdown = value; OnPropertyChanged(); } } + public bool SysShowHibernate { get => _sysShowHibernate; set { _sysShowHibernate = value; OnPropertyChanged(); } } + public bool SysShowLogout { get => _sysShowLogout; set { _sysShowLogout = value; OnPropertyChanged(); } } + public bool SysShowRecycleBin { get => _sysShowRecycleBin; set { _sysShowRecycleBin = value; OnPropertyChanged(); } } + + // ─── 시스템 명령 별칭 (쉼표 구분 문자열) ─────────────────────────────────── + private string _aliasLock = ""; + private string _aliasSleep = ""; + private string _aliasRestart = ""; + private string _aliasShutdown = ""; + private string _aliasHibernate = ""; + private string _aliasLogout = ""; + private string _aliasRecycle = ""; + + public string AliasLock { get => _aliasLock; set { _aliasLock = value; OnPropertyChanged(); } } + public string AliasSleep { get => _aliasSleep; set { _aliasSleep = value; OnPropertyChanged(); } } + public string AliasRestart { get => _aliasRestart; set { _aliasRestart = value; OnPropertyChanged(); } } + public string AliasShutdown { get => _aliasShutdown; set { _aliasShutdown = value; OnPropertyChanged(); } } + public string AliasHibernate { get => _aliasHibernate; set { _aliasHibernate = value; OnPropertyChanged(); } } + public string AliasLogout { get => _aliasLogout; set { _aliasLogout = value; OnPropertyChanged(); } } + public string AliasRecycle { get => _aliasRecycle; set { _aliasRecycle = value; OnPropertyChanged(); } } +} diff --git a/src/AxCopilot/ViewModels/SettingsViewModel.cs b/src/AxCopilot/ViewModels/SettingsViewModel.cs index 49b8924..9b19b52 100644 --- a/src/AxCopilot/ViewModels/SettingsViewModel.cs +++ b/src/AxCopilot/ViewModels/SettingsViewModel.cs @@ -12,132 +12,11 @@ using AxCopilot.Views; namespace AxCopilot.ViewModels; -/// -/// 설정 카드에 표시할 테마 미리보기 데이터 -/// -public class ThemeCardModel : INotifyPropertyChanged -{ - private bool _isSelected; - - public string Key { get; init; } = ""; // settings.json 값 (dark, light, ...) - public string Name { get; init; } = ""; // 표시 이름 - - // 미리보기용 색상 - public string PreviewBackground { get; init; } = "#1A1B2E"; - public string PreviewText { get; init; } = "#F0F0FF"; - public string PreviewSubText { get; init; } = "#7A7D9C"; - public string PreviewAccent { get; init; } = "#4B5EFC"; - public string PreviewItem { get; init; } = "#252637"; - public string PreviewBorder { get; init; } = "#2E2F4A"; - - public bool IsSelected - { - get => _isSelected; - set { _isSelected = value; OnPropertyChanged(); } - } - - public event PropertyChangedEventHandler? PropertyChanged; - protected void OnPropertyChanged([CallerMemberName] string? n = null) - => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); -} - -/// -/// 커스텀 테마 단일 색상 항목 (색상 편집 탭 1행 1색) -/// -public class ColorRowModel : INotifyPropertyChanged -{ - private string _hex; - - public string Label { get; init; } = ""; - public string Property { get; init; } = ""; // CustomThemeColors의 속성명 - - public string Hex - { - get => _hex; - set { _hex = value; OnPropertyChanged(); OnPropertyChanged(nameof(Preview)); } - } - - public SolidColorBrush Preview - { - get - { - try { return ThemeResourceHelper.HexBrush(Hex); } - catch (Exception) { return new SolidColorBrush(Colors.Transparent); } - } - } - - public ColorRowModel(string label, string property, string hex) - { - Label = label; - Property = property; - _hex = hex; - } - - public event PropertyChangedEventHandler? PropertyChanged; - protected void OnPropertyChanged([CallerMemberName] string? n = null) - => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); -} - -/// -/// 빠른 실행 단축키 항목 (키워드 → 앱/URL/폴더) -/// -public class AppShortcutModel : INotifyPropertyChanged -{ - private string _key = ""; - private string _description = ""; - private string _target = ""; - private string _type = "app"; - - public string Key { get => _key; set { _key = value; OnPropertyChanged(); } } - public string Description { get => _description; set { _description = value; OnPropertyChanged(); } } - public string Target { get => _target; set { _target = value; OnPropertyChanged(); } } - public string Type { get => _type; set { _type = value; OnPropertyChanged(); OnPropertyChanged(nameof(TypeSymbol)); OnPropertyChanged(nameof(TypeLabel)); } } - - public string TypeSymbol => Type switch - { - "url" => Symbols.Globe, - "folder" => Symbols.Folder, - _ => Symbols.App - }; - - public string TypeLabel => Type switch - { - "url" => "URL", - "folder" => "폴더", - _ => "앱" - }; - - public event PropertyChangedEventHandler? PropertyChanged; - protected void OnPropertyChanged([CallerMemberName] string? n = null) - => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); -} - -/// -/// 배치 명령 항목 (> 프리픽스) -/// -public class BatchCommandModel : INotifyPropertyChanged -{ - private string _key = ""; - private string _command = ""; - private bool _showWindow; - - public string Key { get => _key; set { _key = value; OnPropertyChanged(); } } - public string Command { get => _command; set { _command = value; OnPropertyChanged(); } } - public bool ShowWindow { get => _showWindow; set { _showWindow = value; OnPropertyChanged(); } } - - public event PropertyChangedEventHandler? PropertyChanged; - protected void OnPropertyChanged([CallerMemberName] string? n = null) - => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); -} - -public class SettingsViewModel : INotifyPropertyChanged +public partial class SettingsViewModel : INotifyPropertyChanged { private readonly SettingsService _service; internal SettingsService Service => _service; - /// CodeSettings 바인딩용 프로퍼티. XAML에서 {Binding Code.EnableLsp} 등으로 접근. - public Models.CodeSettings Code => _service.Settings.Llm.Code; - // ─── 작업 복사본 ─────────────────────────────────────────────────────── private string _hotkey; private int _maxResults; @@ -182,695 +61,6 @@ public class SettingsViewModel : INotifyPropertyChanged private string _claudeApiKey = ""; private string _claudeModel = "claude-sonnet-4-6"; - // 등록 모델 목록 - public ObservableCollection RegisteredModels { get; } = new(); - - public string LlmService - { - get => _llmService; - set { _llmService = value; OnPropertyChanged(); OnPropertyChanged(nameof(IsInternalService)); OnPropertyChanged(nameof(IsExternalService)); OnPropertyChanged(nameof(NeedsEndpoint)); OnPropertyChanged(nameof(NeedsApiKey)); OnPropertyChanged(nameof(IsGeminiSelected)); OnPropertyChanged(nameof(IsClaudeSelected)); } - } - public bool IsInternalService => _llmService is "ollama" or "vllm"; - public bool IsExternalService => _llmService is "gemini" or "claude"; - public bool NeedsEndpoint => _llmService is "ollama" or "vllm"; - public bool NeedsApiKey => _llmService is not "ollama"; - public bool IsGeminiSelected => _llmService == "gemini"; - public bool IsClaudeSelected => _llmService == "claude"; - - // ── Ollama 설정 ── - public string OllamaEndpoint { get => _ollamaEndpoint; set { _ollamaEndpoint = value; OnPropertyChanged(); } } - public string OllamaApiKey { get => _ollamaApiKey; set { _ollamaApiKey = value; OnPropertyChanged(); } } - public string OllamaModel { get => _ollamaModel; set { _ollamaModel = value; OnPropertyChanged(); } } - - // ── vLLM 설정 ── - public string VllmEndpoint { get => _vllmEndpoint; set { _vllmEndpoint = value; OnPropertyChanged(); } } - public string VllmApiKey { get => _vllmApiKey; set { _vllmApiKey = value; OnPropertyChanged(); } } - public string VllmModel { get => _vllmModel; set { _vllmModel = value; OnPropertyChanged(); } } - - // ── Gemini 설정 ── - public string GeminiApiKey { get => _geminiApiKey; set { _geminiApiKey = value; OnPropertyChanged(); } } - public string GeminiModel { get => _geminiModel; set { _geminiModel = value; OnPropertyChanged(); } } - - // ── Claude 설정 ── - public string ClaudeApiKey { get => _claudeApiKey; set { _claudeApiKey = value; OnPropertyChanged(); } } - public string ClaudeModel { get => _claudeModel; set { _claudeModel = value; OnPropertyChanged(); } } - - // ── 공통 응답 설정 ── - public bool LlmStreaming - { - get => _llmStreaming; - set { _llmStreaming = value; OnPropertyChanged(); } - } - public int LlmMaxContextTokens - { - get => _llmMaxContextTokens; - set { _llmMaxContextTokens = value; OnPropertyChanged(); } - } - public int LlmRetentionDays - { - get => _llmRetentionDays; - set { _llmRetentionDays = value; OnPropertyChanged(); } - } - public double LlmTemperature - { - get => _llmTemperature; - set { _llmTemperature = Math.Round(Math.Clamp(value, 0.0, 2.0), 1); OnPropertyChanged(); } - } - - // 에이전트 기본 파일 접근 권한 - private string _defaultAgentPermission; - public string DefaultAgentPermission - { - get => _defaultAgentPermission; - set { _defaultAgentPermission = value; OnPropertyChanged(); } - } - - // ── 코워크/에이전트 고급 설정 ── - private string _defaultOutputFormat; - public string DefaultOutputFormat - { - get => _defaultOutputFormat; - set { _defaultOutputFormat = value; OnPropertyChanged(); } - } - - private string _autoPreview; - public string AutoPreview - { - get => _autoPreview; - set { _autoPreview = value; OnPropertyChanged(); } - } - - private int _maxAgentIterations; - public int MaxAgentIterations - { - get => _maxAgentIterations; - set { _maxAgentIterations = Math.Clamp(value, 1, 100); OnPropertyChanged(); } - } - - private int _maxRetryOnError; - public int MaxRetryOnError - { - get => _maxRetryOnError; - set { _maxRetryOnError = Math.Clamp(value, 0, 10); OnPropertyChanged(); } - } - - private string _agentLogLevel; - public string AgentLogLevel - { - get => _agentLogLevel; - set { _agentLogLevel = value; OnPropertyChanged(); } - } - - private string _agentDecisionLevel = "detailed"; - public string AgentDecisionLevel - { - get => _agentDecisionLevel; - set { _agentDecisionLevel = value; OnPropertyChanged(); } - } - - private string _planMode = "off"; - public string PlanMode - { - get => _planMode; - set { _planMode = value; OnPropertyChanged(); } - } - - private bool _enableMultiPassDocument; - public bool EnableMultiPassDocument - { - get => _enableMultiPassDocument; - set { _enableMultiPassDocument = value; OnPropertyChanged(); } - } - - private bool _enableCoworkVerification; - public bool EnableCoworkVerification - { - get => _enableCoworkVerification; - set { _enableCoworkVerification = value; OnPropertyChanged(); } - } - - private bool _enableFilePathHighlight = true; - public bool EnableFilePathHighlight - { - get => _enableFilePathHighlight; - set { _enableFilePathHighlight = value; OnPropertyChanged(); } - } - - private string _folderDataUsage; - public string FolderDataUsage - { - get => _folderDataUsage; - set { _folderDataUsage = value; OnPropertyChanged(); } - } - - // ── 모델 폴백 + 보안 + MCP ── - private bool _enableAuditLog; - public bool EnableAuditLog - { - get => _enableAuditLog; - set { _enableAuditLog = value; OnPropertyChanged(); } - } - - private bool _enableAgentMemory; - public bool EnableAgentMemory - { - get => _enableAgentMemory; - set { _enableAgentMemory = value; OnPropertyChanged(); } - } - - private bool _enableProjectRules = true; - public bool EnableProjectRules - { - get => _enableProjectRules; - set { _enableProjectRules = value; OnPropertyChanged(); } - } - - private int _maxMemoryEntries; - public int MaxMemoryEntries - { - get => _maxMemoryEntries; - set { _maxMemoryEntries = value; OnPropertyChanged(); } - } - - // ── 이미지 입력 (멀티모달) ── - private bool _enableImageInput = true; - public bool EnableImageInput - { - get => _enableImageInput; - set { _enableImageInput = value; OnPropertyChanged(); } - } - - private int _maxImageSizeKb = 5120; - public int MaxImageSizeKb - { - get => _maxImageSizeKb; - set { _maxImageSizeKb = value; OnPropertyChanged(); } - } - - // ── 자동 모델 라우팅 ── - private bool _enableAutoRouter; - public bool EnableAutoRouter - { - get => _enableAutoRouter; - set { _enableAutoRouter = value; OnPropertyChanged(); } - } - - private double _autoRouterConfidence = 0.7; - public double AutoRouterConfidence - { - get => _autoRouterConfidence; - set { _autoRouterConfidence = value; OnPropertyChanged(); } - } - - // ── 에이전트 훅 시스템 ── - private bool _enableToolHooks = true; - public bool EnableToolHooks - { - get => _enableToolHooks; - set { _enableToolHooks = value; OnPropertyChanged(); } - } - - private int _toolHookTimeoutMs = 10000; - public int ToolHookTimeoutMs - { - get => _toolHookTimeoutMs; - set { _toolHookTimeoutMs = value; OnPropertyChanged(); } - } - - // ── 스킬 시스템 ── - private bool _enableSkillSystem = true; - public bool EnableSkillSystem - { - get => _enableSkillSystem; - set { _enableSkillSystem = value; OnPropertyChanged(); } - } - - private string _skillsFolderPath = ""; - public string SkillsFolderPath - { - get => _skillsFolderPath; - set { _skillsFolderPath = value; OnPropertyChanged(); } - } - - private int _slashPopupPageSize = 6; - public int SlashPopupPageSize - { - get => _slashPopupPageSize; - set { _slashPopupPageSize = Math.Clamp(value, 3, 10); OnPropertyChanged(); } - } - - // ── 드래그&드롭 AI ── - private bool _enableDragDropAiActions = true; - public bool EnableDragDropAiActions - { - get => _enableDragDropAiActions; - set { _enableDragDropAiActions = value; OnPropertyChanged(); } - } - - private bool _dragDropAutoSend; - public bool DragDropAutoSend - { - get => _dragDropAutoSend; - set { _dragDropAutoSend = value; OnPropertyChanged(); } - } - - // ── 코드 리뷰 ── - private bool _enableCodeReview = true; - public bool EnableCodeReview - { - get => _enableCodeReview; - set { _enableCodeReview = value; OnPropertyChanged(); } - } - - // ── 시각 효과 + 알림 + 개발자 모드 (공통) ── - private bool _enableChatRainbowGlow; - public bool EnableChatRainbowGlow - { - get => _enableChatRainbowGlow; - set { _enableChatRainbowGlow = value; OnPropertyChanged(); } - } - - private bool _notifyOnComplete; - public bool NotifyOnComplete - { - get => _notifyOnComplete; - set { _notifyOnComplete = value; OnPropertyChanged(); } - } - - private bool _showTips; - public bool ShowTips - { - get => _showTips; - set { _showTips = value; OnPropertyChanged(); } - } - - private bool _devMode; - public bool DevMode - { - get => _devMode; - set { _devMode = value; OnPropertyChanged(); } - } - - private bool _devModeStepApproval; - public bool DevModeStepApproval - { - get => _devModeStepApproval; - set { _devModeStepApproval = value; OnPropertyChanged(); } - } - - private bool _workflowVisualizer; - public bool WorkflowVisualizer - { - get => _workflowVisualizer; - set { _workflowVisualizer = value; OnPropertyChanged(); } - } - - private bool _freeTierMode; - public bool FreeTierMode - { - get => _freeTierMode; - set { _freeTierMode = value; OnPropertyChanged(); } - } - - private int _freeTierDelaySeconds = 4; - public int FreeTierDelaySeconds - { - get => _freeTierDelaySeconds; - set { _freeTierDelaySeconds = value; OnPropertyChanged(); } - } - - private bool _showTotalCallStats; - public bool ShowTotalCallStats - { - get => _showTotalCallStats; - set { _showTotalCallStats = value; OnPropertyChanged(); } - } - - private string _defaultMood = "modern"; - public string DefaultMood - { - get => _defaultMood; - set { _defaultMood = value; OnPropertyChanged(); } - } - - // 차단 경로/확장자 (읽기 전용 UI) - public ObservableCollection BlockedPaths { get; } = new(); - public ObservableCollection BlockedExtensions { get; } = new(); - - public string Hotkey - { - get => _hotkey; - set { _hotkey = value; OnPropertyChanged(); } - } - - public int MaxResults - { - get => _maxResults; - set { _maxResults = value; OnPropertyChanged(); } - } - - public double Opacity - { - get => _opacity; - set { _opacity = value; OnPropertyChanged(); OnPropertyChanged(nameof(OpacityPercent)); } - } - - public int OpacityPercent => (int)Math.Round(_opacity * 100); - - public string SelectedThemeKey - { - get => _selectedThemeKey; - set - { - _selectedThemeKey = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(IsCustomTheme)); - foreach (var card in ThemeCards) - card.IsSelected = card.Key == value; - } - } - - public bool IsCustomTheme => _selectedThemeKey == "custom"; - - public string LauncherPosition - { - get => _launcherPosition; - set { _launcherPosition = value; OnPropertyChanged(); } - } - - public string WebSearchEngine - { - get => _webSearchEngine; - set { _webSearchEngine = value; OnPropertyChanged(); } - } - - public bool SnippetAutoExpand - { - get => _snippetAutoExpand; - set { _snippetAutoExpand = value; OnPropertyChanged(); } - } - - public string Language - { - get => _language; - set { _language = value; OnPropertyChanged(); } - } - - public string IndexSpeed - { - get => _indexSpeed; - set { _indexSpeed = value; OnPropertyChanged(); OnPropertyChanged(nameof(IndexSpeedHint)); } - } - - public string IndexSpeedHint => _indexSpeed switch - { - "fast" => "CPU 사용률이 높아질 수 있습니다. 고성능 PC에 권장합니다.", - "slow" => "인덱싱이 오래 걸리지만 PC 성능에 영향을 주지 않습니다.", - _ => "일반적인 PC에 적합한 균형 설정입니다.", - }; - - // ─── 기능 토글 속성 ─────────────────────────────────────────────────── - - public bool ShowNumberBadges - { - get => _showNumberBadges; - set { _showNumberBadges = value; OnPropertyChanged(); } - } - - public bool EnableFavorites - { - get => _enableFavorites; - set { _enableFavorites = value; OnPropertyChanged(); } - } - - public bool EnableRecent - { - get => _enableRecent; - set { _enableRecent = value; OnPropertyChanged(); } - } - - public bool EnableActionMode - { - get => _enableActionMode; - set { _enableActionMode = value; OnPropertyChanged(); } - } - - public bool CloseOnFocusLost - { - get => _closeOnFocusLost; - set { _closeOnFocusLost = value; OnPropertyChanged(); } - } - - public bool ShowPrefixBadge - { - get => _showPrefixBadge; - set { _showPrefixBadge = value; OnPropertyChanged(); } - } - - public bool EnableIconAnimation - { - get => _enableIconAnimation; - set { _enableIconAnimation = value; OnPropertyChanged(); } - } - - public bool EnableRainbowGlow - { - get => _enableRainbowGlow; - set { _enableRainbowGlow = value; OnPropertyChanged(); } - } - - public bool EnableSelectionGlow - { - get => _enableSelectionGlow; - set { _enableSelectionGlow = value; OnPropertyChanged(); } - } - - public bool EnableRandomPlaceholder - { - get => _enableRandomPlaceholder; - set { _enableRandomPlaceholder = value; OnPropertyChanged(); } - } - - public bool ShowLauncherBorder - { - get => _showLauncherBorder; - set { _showLauncherBorder = value; OnPropertyChanged(); } - } - - // ─── v1.4.0 신기능 설정 ────────────────────────────────────────────────── - - private bool _enableTextAction = true; - public bool EnableTextAction - { - get => _enableTextAction; - set { _enableTextAction = value; OnPropertyChanged(); } - } - - private bool _enableFileDialogIntegration = false; - public bool EnableFileDialogIntegration - { - get => _enableFileDialogIntegration; - set { _enableFileDialogIntegration = value; OnPropertyChanged(); } - } - - private bool _enableClipboardAutoCategory = true; - public bool EnableClipboardAutoCategory - { - get => _enableClipboardAutoCategory; - set { _enableClipboardAutoCategory = value; OnPropertyChanged(); } - } - - private int _maxPinnedClipboardItems = 20; - public int MaxPinnedClipboardItems - { - get => _maxPinnedClipboardItems; - set { _maxPinnedClipboardItems = value; OnPropertyChanged(); } - } - - private string _textActionTranslateLanguage = "auto"; - public string TextActionTranslateLanguage - { - get => _textActionTranslateLanguage; - set { _textActionTranslateLanguage = value; OnPropertyChanged(); } - } - - private int _maxSubAgents = 3; - public int MaxSubAgents - { - get => _maxSubAgents; - set { _maxSubAgents = value; OnPropertyChanged(); } - } - - private string _pdfExportPath = ""; - public string PdfExportPath - { - get => _pdfExportPath; - set { _pdfExportPath = value; OnPropertyChanged(); } - } - - private int _tipDurationSeconds = 5; - public int TipDurationSeconds - { - get => _tipDurationSeconds; - set { _tipDurationSeconds = value; OnPropertyChanged(); } - } - - public bool ShortcutHelpUseThemeColor - { - get => _shortcutHelpUseThemeColor; - set { _shortcutHelpUseThemeColor = value; OnPropertyChanged(); } - } - - // ─── 테마 카드 목록 ─────────────────────────────────────────────────── - public List ThemeCards { get; } = new() - { - new() - { - Key = "system", Name = "시스템", - PreviewBackground = "#1E1E1E", PreviewText = "#FFFFFF", - PreviewSubText = "#888888", PreviewAccent = "#0078D4", - PreviewItem = "#2D2D2D", PreviewBorder = "#3D3D3D" - }, - new() - { - Key = "dark", Name = "Dark", - PreviewBackground = "#1A1B2E", PreviewText = "#F0F0FF", - PreviewSubText = "#7A7D9C", PreviewAccent = "#4B5EFC", - PreviewItem = "#252637", PreviewBorder = "#2E2F4A" - }, - new() - { - Key = "light", Name = "Light", - PreviewBackground = "#FAFAFA", PreviewText = "#1A1B2E", - PreviewSubText = "#666680", PreviewAccent = "#4B5EFC", - PreviewItem = "#F0F0F8", PreviewBorder = "#E0E0F0" - }, - new() - { - Key = "oled", Name = "OLED", - PreviewBackground = "#000000", PreviewText = "#FFFFFF", - PreviewSubText = "#888899", PreviewAccent = "#5C6EFF", - PreviewItem = "#0A0A14", PreviewBorder = "#1A1A2E" - }, - new() - { - Key = "nord", Name = "Nord", - PreviewBackground = "#2E3440", PreviewText = "#ECEFF4", - PreviewSubText = "#D8DEE9", PreviewAccent = "#88C0D0", - PreviewItem = "#3B4252", PreviewBorder = "#434C5E" - }, - new() - { - Key = "monokai", Name = "Monokai", - PreviewBackground = "#272822", PreviewText = "#F8F8F2", - PreviewSubText = "#75715E", PreviewAccent = "#A6E22E", - PreviewItem = "#3E3D32", PreviewBorder = "#49483E" - }, - new() - { - Key = "catppuccin", Name = "Catppuccin", - PreviewBackground = "#1E1E2E", PreviewText = "#CDD6F4", - PreviewSubText = "#A6ADC8", PreviewAccent = "#CBA6F7", - PreviewItem = "#313244", PreviewBorder = "#45475A" - }, - new() - { - Key = "sepia", Name = "Sepia", - PreviewBackground = "#F5EFE0", PreviewText = "#3C2F1A", - PreviewSubText = "#7A6040", PreviewAccent = "#C0822A", - PreviewItem = "#EDE6D6", PreviewBorder = "#D8CCBA" - }, - new() - { - Key = "alfred", Name = "Indigo", - PreviewBackground = "#26273B", PreviewText = "#EEEEFF", - PreviewSubText = "#8888BB", PreviewAccent = "#8877EE", - PreviewItem = "#3B3D60", PreviewBorder = "#40416A" - }, - new() - { - Key = "alfredlight", Name = "Frost", - PreviewBackground = "#FFFFFF", PreviewText = "#1A1A2E", - PreviewSubText = "#9090AA", PreviewAccent = "#5555EE", - PreviewItem = "#E8E9FF", PreviewBorder = "#DCDCEE" - }, - new() - { - Key = "codex", Name = "Codex", - PreviewBackground = "#FFFFFF", PreviewText = "#111111", - PreviewSubText = "#6B7280", PreviewAccent = "#7C3AED", - PreviewItem = "#F5F5F7", PreviewBorder = "#E5E7EB" - }, - new() - { - Key = "custom", Name = "커스텀", - PreviewBackground = "#1A1B2E", PreviewText = "#F0F0FF", - PreviewSubText = "#7A7D9C", PreviewAccent = "#4B5EFC", - PreviewItem = "#252637", PreviewBorder = "#2E2F4A" - }, - }; - - // ─── 커스텀 색상 행 목록 ────────────────────────────────────────────── - public List ColorRows { get; } - - // ─── 프롬프트 템플릿 ────────────────────────────────────────────────── - public ObservableCollection PromptTemplates { get; } = new(); - - // ─── 배치 명령 ──────────────────────────────────────────────────────── - public ObservableCollection BatchCommands { get; } = new(); - - private string _newBatchKey = ""; - private string _newBatchCommand = ""; - private bool _newBatchShowWindow; - - public string NewBatchKey - { - get => _newBatchKey; - set { _newBatchKey = value; OnPropertyChanged(); } - } - public string NewBatchCommand - { - get => _newBatchCommand; - set { _newBatchCommand = value; OnPropertyChanged(); } - } - public bool NewBatchShowWindow - { - get => _newBatchShowWindow; - set { _newBatchShowWindow = value; OnPropertyChanged(); } - } - - public bool AddBatchCommand() - { - if (string.IsNullOrWhiteSpace(_newBatchKey) || string.IsNullOrWhiteSpace(_newBatchCommand)) - return false; - if (BatchCommands.Any(b => b.Key.Equals(_newBatchKey.Trim(), StringComparison.OrdinalIgnoreCase))) - return false; - - BatchCommands.Add(new BatchCommandModel - { - Key = _newBatchKey.Trim(), - Command = _newBatchCommand.Trim(), - ShowWindow = _newBatchShowWindow - }); - NewBatchKey = ""; NewBatchCommand = ""; NewBatchShowWindow = false; - return true; - } - - public void RemoveBatchCommand(BatchCommandModel cmd) => BatchCommands.Remove(cmd); - - // ─── 빠른 실행 단축키 ───────────────────────────────────────────────── - public ObservableCollection AppShortcuts { get; } = new(); - - private string _newKey = ""; - private string _newDescription = ""; - private string _newTarget = ""; - private string _newType = "app"; - - public string NewKey { get => _newKey; set { _newKey = value; OnPropertyChanged(); } } - public string NewDescription { get => _newDescription; set { _newDescription = value; OnPropertyChanged(); } } - public string NewTarget { get => _newTarget; set { _newTarget = value; OnPropertyChanged(); } } - public string NewType { get => _newType; set { _newType = value; OnPropertyChanged(); } } - // ─── 이벤트 ─────────────────────────────────────────────────────────── public event EventHandler? ThemePreviewRequested; public event EventHandler? SaveCompleted; @@ -1127,729 +317,4 @@ public class SettingsViewModel : INotifyPropertyChanged _customWindowCornerRadius = c.WindowCornerRadius; _customItemCornerRadius = c.ItemCornerRadius; } - - // ─── 커스텀 테마 모서리 라운딩 ─────────────────────────────────────────── - private int _customWindowCornerRadius = 20; - private int _customItemCornerRadius = 10; - - public int CustomWindowCornerRadius - { - get => _customWindowCornerRadius; - set { _customWindowCornerRadius = Math.Clamp(value, 0, 30); OnPropertyChanged(); } - } - - public int CustomItemCornerRadius - { - get => _customItemCornerRadius; - set { _customItemCornerRadius = Math.Clamp(value, 0, 20); OnPropertyChanged(); } - } - - // ─── 인덱스 경로 ────────────────────────────────────────────────────── - public ObservableCollection IndexPaths { get; } = new(); - - public void AddIndexPath(string path) - { - var trimmed = path.Trim(); - if (string.IsNullOrWhiteSpace(trimmed)) return; - if (IndexPaths.Any(p => p.Equals(trimmed, StringComparison.OrdinalIgnoreCase))) return; - IndexPaths.Add(trimmed); - } - - public void RemoveIndexPath(string path) => IndexPaths.Remove(path); - - // ─── 인덱스 확장자 ────────────────────────────────────────────────── - public ObservableCollection IndexExtensions { get; } = new(); - - public void AddExtension(string ext) - { - var trimmed = ext.Trim().ToLowerInvariant(); - if (string.IsNullOrWhiteSpace(trimmed)) return; - if (!trimmed.StartsWith(".")) trimmed = "." + trimmed; - if (IndexExtensions.Any(e => e.Equals(trimmed, StringComparison.OrdinalIgnoreCase))) return; - IndexExtensions.Add(trimmed); - } - - public void RemoveExtension(string ext) => IndexExtensions.Remove(ext); - - public void BrowseIndexPath() - { - using var dlg = new FolderBrowserDialog - { - Description = "인덱스할 폴더 선택", - UseDescriptionForTitle = true, - ShowNewFolderButton = false - }; - if (dlg.ShowDialog() != DialogResult.OK) return; - AddIndexPath(dlg.SelectedPath); - } - - // ─── 빠른 실행 단축키 메서드 ────────────────────────────────────────── - public bool AddShortcut() - { - if (string.IsNullOrWhiteSpace(_newKey) || string.IsNullOrWhiteSpace(_newTarget)) - return false; - - // 중복 키 확인 - if (AppShortcuts.Any(s => s.Key.Equals(_newKey.Trim(), StringComparison.OrdinalIgnoreCase))) - return false; - - AppShortcuts.Add(new AppShortcutModel - { - Key = _newKey.Trim(), - Description = _newDescription.Trim(), - Target = _newTarget.Trim(), - Type = _newType - }); - - NewKey = ""; NewDescription = ""; NewTarget = ""; NewType = "app"; - return true; - } - - public void RemoveShortcut(AppShortcutModel shortcut) => AppShortcuts.Remove(shortcut); - - /// 파일 선택 대화상자. 선택 시 NewTarget에 자동 설정. - public void BrowseTarget() - { - using var dlg = new OpenFileDialog - { - Filter = "실행 파일 (*.exe)|*.exe|모든 파일 (*.*)|*.*", - Title = "앱 선택" - }; - if (dlg.ShowDialog() != DialogResult.OK) return; - - NewTarget = dlg.FileName; - NewType = "app"; - if (string.IsNullOrWhiteSpace(NewDescription)) - NewDescription = System.IO.Path.GetFileNameWithoutExtension(dlg.FileName); - } - - public void SelectTheme(string key) - { - SelectedThemeKey = key; - ThemePreviewRequested?.Invoke(this, EventArgs.Empty); - } - - public void PickColor(ColorRowModel row) - { - using var dlg = new ColorDialog { FullOpen = true }; - try - { - var color = ThemeResourceHelper.HexColor(row.Hex); - dlg.Color = System.Drawing.Color.FromArgb(color.R, color.G, color.B); - } - catch (Exception) { /* 기본값 사용 */ } - - if (dlg.ShowDialog() != DialogResult.OK) return; - - row.Hex = $"#{dlg.Color.R:X2}{dlg.Color.G:X2}{dlg.Color.B:X2}"; - if (_selectedThemeKey == "custom") - ThemePreviewRequested?.Invoke(this, EventArgs.Empty); - } - - // 시스템 예약 프리픽스 (핸들러에서 이미 사용 중인 키) - private static readonly HashSet ReservedPrefixes = new(StringComparer.OrdinalIgnoreCase) - { - "=", "?", "#", "$", ";", "@", "~", ">", "!", - "emoji", "color", "recent", "note", "uninstall", "kill", "media", - "info", "*", "json", "encode", "port", "env", "snap", "help", - "pick", "date", "svc", "pipe", "journal", "routine", "batch", - "diff", "win", "stats", "fav", "rename", "monitor", "scaffold", - }; - - /// 설정 저장 전 프리픽스/키워드 충돌을 검사합니다. 충돌 시 메시지를 반환합니다. - public string? ValidateBeforeSave() - { - // 캡처 프리픽스 충돌 검사 - var cap = string.IsNullOrWhiteSpace(_capPrefix) ? "cap" : _capPrefix.Trim(); - if (cap != "cap" && ReservedPrefixes.Contains(cap)) - return $"캡처 프리픽스 '{cap}'은(는) 이미 사용 중인 예약어입니다."; - - // 빠른 실행 별칭 키 중복 검사 - var aliasKeys = AppShortcuts.Select(s => s.Key.ToLowerInvariant()).ToList(); - var duplicateAlias = aliasKeys.GroupBy(k => k).FirstOrDefault(g => g.Count() > 1); - if (duplicateAlias != null) - return $"빠른 실행 키워드 '{duplicateAlias.Key}'이(가) 중복되었습니다."; - - // 빠른 실행 별칭 키 vs 예약 프리픽스 충돌 - foreach (var key in aliasKeys) - { - if (ReservedPrefixes.Contains(key)) - return $"빠른 실행 키워드 '{key}'은(는) 시스템 예약어와 충돌합니다."; - } - - // 배치 명령 키 중복 검사 - var batchKeys = BatchCommands.Select(b => b.Key.ToLowerInvariant()).ToList(); - var duplicateBatch = batchKeys.GroupBy(k => k).FirstOrDefault(g => g.Count() > 1); - if (duplicateBatch != null) - return $"배치 명령 키워드 '{duplicateBatch.Key}'이(가) 중복되었습니다."; - - return null; // 문제 없음 - } - - public void Save() - { - // 충돌 검사 - var conflict = ValidateBeforeSave(); - if (conflict != null) - { - CustomMessageBox.Show( - conflict, - "AX Copilot — 설정 저장 오류", - System.Windows.MessageBoxButton.OK, - System.Windows.MessageBoxImage.Warning); - return; - } - - var s = _service.Settings; - s.Hotkey = _hotkey; - s.Launcher.MaxResults = _maxResults; - s.Launcher.Opacity = _opacity; - s.Launcher.Theme = _selectedThemeKey; - s.Launcher.Position = _launcherPosition; - s.Launcher.WebSearchEngine = _webSearchEngine; - s.Launcher.SnippetAutoExpand = _snippetAutoExpand; - s.Launcher.Language = _language; - L10n.SetLanguage(_language); - - // 기능 토글 저장 - s.Launcher.ShowNumberBadges = _showNumberBadges; - s.Launcher.EnableFavorites = _enableFavorites; - s.Launcher.EnableRecent = _enableRecent; - s.Launcher.EnableActionMode = _enableActionMode; - s.Launcher.CloseOnFocusLost = _closeOnFocusLost; - s.Launcher.ShowPrefixBadge = _showPrefixBadge; - s.Launcher.EnableIconAnimation = _enableIconAnimation; - s.Launcher.EnableRainbowGlow = _enableRainbowGlow; - s.Launcher.EnableSelectionGlow = _enableSelectionGlow; - s.Launcher.EnableRandomPlaceholder = _enableRandomPlaceholder; - s.Launcher.ShowLauncherBorder = _showLauncherBorder; - s.Launcher.ShortcutHelpUseThemeColor = _shortcutHelpUseThemeColor; - s.Launcher.EnableTextAction = _enableTextAction; - s.Launcher.EnableFileDialogIntegration = _enableFileDialogIntegration; - s.Launcher.EnableClipboardAutoCategory = _enableClipboardAutoCategory; - s.Launcher.MaxPinnedClipboardItems = _maxPinnedClipboardItems; - s.Launcher.TextActionTranslateLanguage = _textActionTranslateLanguage; - s.Llm.MaxSubAgents = _maxSubAgents; - s.Llm.PdfExportPath = _pdfExportPath; - s.Llm.TipDurationSeconds = _tipDurationSeconds; - - // LLM 공통 설정 저장 - s.Llm.Service = _llmService; - s.Llm.Streaming = _llmStreaming; - s.Llm.MaxContextTokens = _llmMaxContextTokens; - s.Llm.RetentionDays = _llmRetentionDays; - s.Llm.Temperature = _llmTemperature; - s.Llm.DefaultAgentPermission = _defaultAgentPermission; - s.Llm.DefaultOutputFormat = _defaultOutputFormat; - s.Llm.DefaultMood = _defaultMood; - s.Llm.AutoPreview = _autoPreview; - s.Llm.MaxAgentIterations = _maxAgentIterations; - s.Llm.MaxRetryOnError = _maxRetryOnError; - s.Llm.AgentLogLevel = _agentLogLevel; - s.Llm.AgentDecisionLevel = _agentDecisionLevel; - s.Llm.PlanMode = _planMode; - s.Llm.EnableMultiPassDocument = _enableMultiPassDocument; - s.Llm.EnableCoworkVerification = _enableCoworkVerification; - s.Llm.EnableFilePathHighlight = _enableFilePathHighlight; - s.Llm.FolderDataUsage = _folderDataUsage; - s.Llm.EnableAuditLog = _enableAuditLog; - s.Llm.EnableAgentMemory = _enableAgentMemory; - s.Llm.EnableProjectRules = _enableProjectRules; - s.Llm.MaxMemoryEntries = _maxMemoryEntries; - s.Llm.EnableImageInput = _enableImageInput; - s.Llm.MaxImageSizeKb = _maxImageSizeKb; - s.Llm.EnableToolHooks = _enableToolHooks; - s.Llm.ToolHookTimeoutMs = _toolHookTimeoutMs; - s.Llm.EnableSkillSystem = _enableSkillSystem; - s.Llm.SkillsFolderPath = _skillsFolderPath; - s.Llm.SlashPopupPageSize = _slashPopupPageSize; - s.Llm.EnableDragDropAiActions = _enableDragDropAiActions; - s.Llm.DragDropAutoSend = _dragDropAutoSend; - s.Llm.Code.EnableCodeReview = _enableCodeReview; - s.Llm.EnableAutoRouter = _enableAutoRouter; - s.Llm.AutoRouterConfidence = _autoRouterConfidence; - s.Llm.EnableChatRainbowGlow = _enableChatRainbowGlow; - s.Llm.NotifyOnComplete = _notifyOnComplete; - s.Llm.ShowTips = _showTips; - s.Llm.DevMode = _devMode; - s.Llm.DevModeStepApproval = _devModeStepApproval; - s.Llm.WorkflowVisualizer = _workflowVisualizer; - s.Llm.FreeTierMode = _freeTierMode; - s.Llm.FreeTierDelaySeconds = _freeTierDelaySeconds; - s.Llm.ShowTotalCallStats = _showTotalCallStats; - - // 서비스별 독립 설정 저장 - s.Llm.OllamaEndpoint = _ollamaEndpoint; - s.Llm.OllamaModel = _ollamaModel; - s.Llm.VllmEndpoint = _vllmEndpoint; - s.Llm.VllmModel = _vllmModel; - s.Llm.GeminiModel = _geminiModel; - s.Llm.ClaudeModel = _claudeModel; - s.Llm.GeminiApiKey = _geminiApiKey; - s.Llm.ClaudeApiKey = _claudeApiKey; - - // 내부 서비스 API 키 저장 (암호화 분기) - if (!string.IsNullOrEmpty(_ollamaApiKey) && _ollamaApiKey != "(저장됨)") - s.Llm.OllamaApiKey = CryptoService.EncryptIfEnabled(_ollamaApiKey, s.Llm.EncryptionEnabled); - if (!string.IsNullOrEmpty(_vllmApiKey) && _vllmApiKey != "(저장됨)") - s.Llm.VllmApiKey = CryptoService.EncryptIfEnabled(_vllmApiKey, s.Llm.EncryptionEnabled); - - // 활성 서비스의 설정을 기존 호환 필드에도 동기화 (LlmService.cs 호환) - switch (_llmService) - { - case "ollama": - s.Llm.Endpoint = _ollamaEndpoint; - s.Llm.Model = _ollamaModel; - s.Llm.EncryptedApiKey = s.Llm.OllamaApiKey; - break; - case "vllm": - s.Llm.Endpoint = _vllmEndpoint; - s.Llm.Model = _vllmModel; - s.Llm.EncryptedApiKey = s.Llm.VllmApiKey; - break; - case "gemini": - s.Llm.ApiKey = _geminiApiKey; - s.Llm.Model = _geminiModel; - break; - case "claude": - s.Llm.ApiKey = _claudeApiKey; - s.Llm.Model = _claudeModel; - break; - } - - // 등록 모델 저장 - s.Llm.RegisteredModels = RegisteredModels - .Where(rm => !string.IsNullOrWhiteSpace(rm.Alias)) - .Select(rm => new RegisteredModel - { - Alias = rm.Alias, - EncryptedModelName = rm.EncryptedModelName, - Service = rm.Service, - Endpoint = rm.Endpoint, - ApiKey = rm.ApiKey, - AuthType = rm.AuthType ?? "bearer", - Cp4dUrl = rm.Cp4dUrl ?? "", - Cp4dUsername = rm.Cp4dUsername ?? "", - Cp4dPassword = rm.Cp4dPassword ?? "", - }) - .ToList(); - - // 프롬프트 템플릿 저장 - s.Llm.PromptTemplates = PromptTemplates - .Where(pt => !string.IsNullOrWhiteSpace(pt.Name)) - .Select(pt => new PromptTemplate { Name = pt.Name, Content = pt.Content, Icon = pt.Icon }) - .ToList(); - - // 인덱스 경로 + 확장자 저장 - s.IndexPaths = IndexPaths.ToList(); - s.IndexExtensions = IndexExtensions.ToList(); - s.IndexSpeed = _indexSpeed; - - // 커스텀 색상 + 모양 저장 - var c = s.Launcher.CustomTheme ??= new CustomThemeColors(); - foreach (var row in ColorRows) - { - var prop = typeof(CustomThemeColors).GetProperty(row.Property); - prop?.SetValue(c, row.Hex); - } - c.WindowCornerRadius = _customWindowCornerRadius; - c.ItemCornerRadius = _customItemCornerRadius; - - // 빠른 실행 단축키 저장: - // batch/api/clipboard type은 그대로 유지, app/url/folder는 ViewModel 내용으로 교체 - var otherAliases = s.Aliases - .Where(a => a.Type is not ("app" or "url" or "folder" or "batch")) - .ToList(); - s.Aliases = otherAliases - .Concat(AppShortcuts.Select(sc => new AliasEntry - { - Key = sc.Key, - Type = sc.Type, - Target = sc.Target, - Description = string.IsNullOrWhiteSpace(sc.Description) ? null : sc.Description - })) - .Concat(BatchCommands.Select(b => new AliasEntry - { - Key = b.Key, - Type = "batch", - Target = b.Command, - ShowWindow = b.ShowWindow - })) - .ToList(); - - // 스니펫 저장 - s.Snippets = Snippets.Select(sn => new SnippetEntry - { - Key = sn.Key, - Name = sn.Name, - Content = sn.Content - }).ToList(); - - // 알림 설정 저장 - s.Reminder.Enabled = _reminderEnabled; - s.Reminder.Corner = _reminderCorner; - s.Reminder.IntervalMinutes = _reminderIntervalMinutes; - s.Reminder.DisplaySeconds = _reminderDisplaySeconds; - - // 캡처 설정 저장 - s.ScreenCapture.Prefix = string.IsNullOrWhiteSpace(_capPrefix) ? "cap" : _capPrefix.Trim(); - s.ScreenCapture.GlobalHotkeyEnabled = _capGlobalHotkeyEnabled; - s.ScreenCapture.GlobalHotkey = string.IsNullOrWhiteSpace(_capGlobalHotkey) ? "PrintScreen" : _capGlobalHotkey.Trim(); - s.ScreenCapture.GlobalHotkeyMode = _capGlobalMode; - s.ScreenCapture.ScrollDelayMs = Math.Max(50, _capScrollDelayMs); - - // 클립보드 히스토리 설정 저장 - s.ClipboardHistory.Enabled = _clipboardEnabled; - s.ClipboardHistory.MaxItems = _clipboardMaxItems; - - // 시스템 명령 설정 저장 - var sc = s.SystemCommands; - sc.ShowLock = _sysShowLock; - sc.ShowSleep = _sysShowSleep; - sc.ShowRestart = _sysShowRestart; - sc.ShowShutdown = _sysShowShutdown; - sc.ShowHibernate = _sysShowHibernate; - sc.ShowLogout = _sysShowLogout; - sc.ShowRecycleBin = _sysShowRecycleBin; - - // 시스템 명령 별칭 저장 - var cmdAliases = new Dictionary>(); - void SaveAlias(string key, string val) - { - var list = ParseAliases(val); - if (list.Count > 0) cmdAliases[key] = list; - } - SaveAlias("lock", _aliasLock); - SaveAlias("sleep", _aliasSleep); - SaveAlias("restart", _aliasRestart); - SaveAlias("shutdown", _aliasShutdown); - SaveAlias("hibernate", _aliasHibernate); - SaveAlias("logout", _aliasLogout); - SaveAlias("recycle", _aliasRecycle); - sc.CommandAliases = cmdAliases; - - _service.Save(); - SaveCompleted?.Invoke(this, EventArgs.Empty); - } - - // ─── 스니펫 설정 ────────────────────────────────────────────────────────── - public ObservableCollection Snippets { get; } = new(); - - private string _newSnippetKey = ""; - private string _newSnippetName = ""; - private string _newSnippetContent = ""; - - public string NewSnippetKey { get => _newSnippetKey; set { _newSnippetKey = value; OnPropertyChanged(); } } - public string NewSnippetName { get => _newSnippetName; set { _newSnippetName = value; OnPropertyChanged(); } } - public string NewSnippetContent { get => _newSnippetContent; set { _newSnippetContent = value; OnPropertyChanged(); } } - - public bool AddSnippet() - { - if (string.IsNullOrWhiteSpace(_newSnippetKey) || string.IsNullOrWhiteSpace(_newSnippetContent)) - return false; - if (Snippets.Any(sn => sn.Key.Equals(_newSnippetKey.Trim(), StringComparison.OrdinalIgnoreCase))) - return false; - - Snippets.Add(new SnippetRowModel - { - Key = _newSnippetKey.Trim(), - Name = _newSnippetName.Trim(), - Content = _newSnippetContent.Trim() - }); - NewSnippetKey = ""; NewSnippetName = ""; NewSnippetContent = ""; - return true; - } - - public void RemoveSnippet(SnippetRowModel row) => Snippets.Remove(row); - - // ─── 클립보드 히스토리 설정 ──────────────────────────────────────────────── - private bool _clipboardEnabled; - private int _clipboardMaxItems; - - public bool ClipboardEnabled - { - get => _clipboardEnabled; - set { _clipboardEnabled = value; OnPropertyChanged(); } - } - - public int ClipboardMaxItems - { - get => _clipboardMaxItems; - set { _clipboardMaxItems = value; OnPropertyChanged(); } - } - - // ─── 스크린 캡처 설정 ────────────────────────────────────────────────────── - private string _capPrefix = "cap"; - private bool _capGlobalHotkeyEnabled; - private string _capGlobalHotkey = "PrintScreen"; - private string _capGlobalMode = "screen"; - private int _capScrollDelayMs = 120; - - public string CapPrefix - { - get => _capPrefix; - set { _capPrefix = value; OnPropertyChanged(); } - } - - public void ResetCapPrefix() => CapPrefix = "cap"; - - public bool CapGlobalHotkeyEnabled - { - get => _capGlobalHotkeyEnabled; - set { _capGlobalHotkeyEnabled = value; OnPropertyChanged(); } - } - - public string CapGlobalHotkey - { - get => _capGlobalHotkey; - set { _capGlobalHotkey = value; OnPropertyChanged(); } - } - - public string CapGlobalMode - { - get => _capGlobalMode; - set { _capGlobalMode = value; OnPropertyChanged(); } - } - - public int CapScrollDelayMs - { - get => _capScrollDelayMs; - set { _capScrollDelayMs = value; OnPropertyChanged(); OnPropertyChanged(nameof(CapScrollDelayMsStr)); } - } - - /// ComboBox SelectedValue와 string 바인딩 용도 - public string CapScrollDelayMsStr - { - get => _capScrollDelayMs.ToString(); - set { if (int.TryParse(value, out int v)) CapScrollDelayMs = v; } - } - - public void ResetCapGlobalHotkey() - { - CapGlobalHotkey = "PrintScreen"; - CapGlobalMode = "screen"; - } - - // ─── 잠금 해제 알림 설정 ────────────────────────────────────────────────── - private bool _reminderEnabled; - private string _reminderCorner = "bottom-right"; - private int _reminderIntervalMinutes = 60; - private int _reminderDisplaySeconds = 15; - - public List GetReminderCategories() => _service.Settings.Reminder.EnabledCategories; - - public bool ReminderEnabled - { - get => _reminderEnabled; - set { _reminderEnabled = value; OnPropertyChanged(); } - } - - public string ReminderCorner - { - get => _reminderCorner; - set { _reminderCorner = value; OnPropertyChanged(); } - } - - public int ReminderIntervalMinutes - { - get => _reminderIntervalMinutes; - set { _reminderIntervalMinutes = value; OnPropertyChanged(); } - } - - public int ReminderDisplaySeconds - { - get => _reminderDisplaySeconds; - set { _reminderDisplaySeconds = value; OnPropertyChanged(); } - } - - // ─── 시스템 명령 설정 ────────────────────────────────────────────────────── - private bool _sysShowLock; - private bool _sysShowSleep; - private bool _sysShowRestart; - private bool _sysShowShutdown; - private bool _sysShowHibernate; - private bool _sysShowLogout; - private bool _sysShowRecycleBin; - - public bool SysShowLock { get => _sysShowLock; set { _sysShowLock = value; OnPropertyChanged(); } } - public bool SysShowSleep { get => _sysShowSleep; set { _sysShowSleep = value; OnPropertyChanged(); } } - public bool SysShowRestart { get => _sysShowRestart; set { _sysShowRestart = value; OnPropertyChanged(); } } - public bool SysShowShutdown { get => _sysShowShutdown; set { _sysShowShutdown = value; OnPropertyChanged(); } } - public bool SysShowHibernate { get => _sysShowHibernate; set { _sysShowHibernate = value; OnPropertyChanged(); } } - public bool SysShowLogout { get => _sysShowLogout; set { _sysShowLogout = value; OnPropertyChanged(); } } - public bool SysShowRecycleBin { get => _sysShowRecycleBin; set { _sysShowRecycleBin = value; OnPropertyChanged(); } } - - // ─── 시스템 명령 별칭 (쉼표 구분 문자열) ─────────────────────────────────── - private string _aliasLock = ""; - private string _aliasSleep = ""; - private string _aliasRestart = ""; - private string _aliasShutdown = ""; - private string _aliasHibernate = ""; - private string _aliasLogout = ""; - private string _aliasRecycle = ""; - - public string AliasLock { get => _aliasLock; set { _aliasLock = value; OnPropertyChanged(); } } - public string AliasSleep { get => _aliasSleep; set { _aliasSleep = value; OnPropertyChanged(); } } - public string AliasRestart { get => _aliasRestart; set { _aliasRestart = value; OnPropertyChanged(); } } - public string AliasShutdown { get => _aliasShutdown; set { _aliasShutdown = value; OnPropertyChanged(); } } - public string AliasHibernate { get => _aliasHibernate; set { _aliasHibernate = value; OnPropertyChanged(); } } - public string AliasLogout { get => _aliasLogout; set { _aliasLogout = value; OnPropertyChanged(); } } - public string AliasRecycle { get => _aliasRecycle; set { _aliasRecycle = value; OnPropertyChanged(); } } - - public void ResetSystemCommandAliases() - { - AliasLock = ""; AliasSleep = ""; AliasRestart = ""; - AliasShutdown = ""; AliasHibernate = ""; AliasLogout = ""; AliasRecycle = ""; - } - - private static string FormatAliases(Dictionary> dict, string key) - => dict.TryGetValue(key, out var list) ? string.Join(", ", list) : ""; - - private static List ParseAliases(string input) - => input.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Where(s => !string.IsNullOrWhiteSpace(s)) - .ToList(); - - public event PropertyChangedEventHandler? PropertyChanged; - protected void OnPropertyChanged([CallerMemberName] string? n = null) - => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); -} - -// ─── 프롬프트 템플릿 행 모델 ───────────────────────────────────────────────────── - -public class PromptTemplateRow : INotifyPropertyChanged -{ - private string _name = ""; - private string _content = ""; - private string _icon = "\uE8BD"; - - public string Name { get => _name; set { _name = value; OnPropertyChanged(); } } - public string Content { get => _content; set { _content = value; OnPropertyChanged(); OnPropertyChanged(nameof(Preview)); } } - public string Icon { get => _icon; set { _icon = value; OnPropertyChanged(); } } - - public string Preview => Content.Length > 60 - ? Content[..57].Replace("\n", " ") + "…" - : Content.Replace("\n", " "); - - public event PropertyChangedEventHandler? PropertyChanged; - protected void OnPropertyChanged([CallerMemberName] string? n = null) - => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); -} - -// ─── 등록 모델 행 모델 ────────────────────────────────────────────────────────── - -public class RegisteredModelRow : INotifyPropertyChanged -{ - private string _alias = ""; - private string _encryptedModelName = ""; - private string _service = "ollama"; - private string _endpoint = ""; - private string _apiKey = ""; - - /// UI 표시용 별칭 - public string Alias - { - get => _alias; - set { _alias = value; OnPropertyChanged(); } - } - - /// 암호화된 실제 모델명 (PortableEncrypt) - public string EncryptedModelName - { - get => _encryptedModelName; - set { _encryptedModelName = value; OnPropertyChanged(); OnPropertyChanged(nameof(MaskedModelName)); } - } - - /// 서비스 타입 (ollama / vllm) - public string Service - { - get => _service; - set { _service = value; OnPropertyChanged(); OnPropertyChanged(nameof(ServiceLabel)); } - } - - /// 이 모델 전용 서버 엔드포인트. 비어있으면 기본 엔드포인트 사용. - public string Endpoint - { - get => _endpoint; - set { _endpoint = value; OnPropertyChanged(); OnPropertyChanged(nameof(EndpointDisplay)); } - } - - /// 이 모델 전용 API 키. 비어있으면 기본 API 키 사용. - public string ApiKey - { - get => _apiKey; - set { _apiKey = value; OnPropertyChanged(); } - } - - // ── CP4D 인증 필드 ────────────────────────────────────────────────── - - private string _authType = "bearer"; - private string _cp4dUrl = ""; - private string _cp4dUsername = ""; - private string _cp4dPassword = ""; - - /// 인증 방식. bearer | cp4d - public string AuthType - { - get => _authType; - set { _authType = value; OnPropertyChanged(); OnPropertyChanged(nameof(AuthLabel)); } - } - - /// CP4D 서버 URL - public string Cp4dUrl - { - get => _cp4dUrl; - set { _cp4dUrl = value; OnPropertyChanged(); } - } - - /// CP4D 사용자 이름 - public string Cp4dUsername - { - get => _cp4dUsername; - set { _cp4dUsername = value; OnPropertyChanged(); } - } - - /// CP4D 비밀번호 (암호화 저장) - public string Cp4dPassword - { - get => _cp4dPassword; - set { _cp4dPassword = value; OnPropertyChanged(); } - } - - /// 인증 방식 라벨 - public string AuthLabel => _authType == "cp4d" ? "CP4D" : "Bearer"; - - /// UI에 표시할 엔드포인트 요약 - public string EndpointDisplay => string.IsNullOrEmpty(_endpoint) ? "(기본 서버)" : _endpoint; - - /// UI에 표시할 마스킹된 모델명 - public string MaskedModelName => string.IsNullOrEmpty(_encryptedModelName) ? "(미등록)" : "••••••••"; - - /// 서비스 라벨 - public string ServiceLabel => _service == "vllm" ? "vLLM" : "Ollama"; - - public event PropertyChangedEventHandler? PropertyChanged; - protected void OnPropertyChanged([CallerMemberName] string? n = null) - => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); -} - -// ─── 스니펫 행 모델 ──────────────────────────────────────────────────────────── - -public class SnippetRowModel : INotifyPropertyChanged -{ - private string _key = ""; - private string _name = ""; - private string _content = ""; - - public string Key { get => _key; set { _key = value; OnPropertyChanged(); } } - public string Name { get => _name; set { _name = value; OnPropertyChanged(); } } - public string Content { get => _content; set { _content = value; OnPropertyChanged(); } } - - public string Preview => Content.Length > 50 - ? Content[..47].Replace("\n", " ") + "…" - : Content.Replace("\n", " "); - - public event PropertyChangedEventHandler? PropertyChanged; - protected void OnPropertyChanged([CallerMemberName] string? n = null) - => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); } diff --git a/src/AxCopilot/ViewModels/SettingsViewModelModels.cs b/src/AxCopilot/ViewModels/SettingsViewModelModels.cs new file mode 100644 index 0000000..97a7834 --- /dev/null +++ b/src/AxCopilot/ViewModels/SettingsViewModelModels.cs @@ -0,0 +1,265 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows.Media; +using AxCopilot.Themes; +using AxCopilot.Views; + +namespace AxCopilot.ViewModels; + +/// +/// 설정 카드에 표시할 테마 미리보기 데이터 +/// +public class ThemeCardModel : INotifyPropertyChanged +{ + private bool _isSelected; + + public string Key { get; init; } = ""; // settings.json 값 (dark, light, ...) + public string Name { get; init; } = ""; // 표시 이름 + + // 미리보기용 색상 + public string PreviewBackground { get; init; } = "#1A1B2E"; + public string PreviewText { get; init; } = "#F0F0FF"; + public string PreviewSubText { get; init; } = "#7A7D9C"; + public string PreviewAccent { get; init; } = "#4B5EFC"; + public string PreviewItem { get; init; } = "#252637"; + public string PreviewBorder { get; init; } = "#2E2F4A"; + + public bool IsSelected + { + get => _isSelected; + set { _isSelected = value; OnPropertyChanged(); } + } + + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged([CallerMemberName] string? n = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); +} + +/// +/// 커스텀 테마 단일 색상 항목 (색상 편집 탭 1행 1색) +/// +public class ColorRowModel : INotifyPropertyChanged +{ + private string _hex; + + public string Label { get; init; } = ""; + public string Property { get; init; } = ""; // CustomThemeColors의 속성명 + + public string Hex + { + get => _hex; + set { _hex = value; OnPropertyChanged(); OnPropertyChanged(nameof(Preview)); } + } + + public SolidColorBrush Preview + { + get + { + try { return ThemeResourceHelper.HexBrush(Hex); } + catch (Exception) { return new SolidColorBrush(Colors.Transparent); } + } + } + + public ColorRowModel(string label, string property, string hex) + { + Label = label; + Property = property; + _hex = hex; + } + + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged([CallerMemberName] string? n = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); +} + +/// +/// 빠른 실행 단축키 항목 (키워드 → 앱/URL/폴더) +/// +public class AppShortcutModel : INotifyPropertyChanged +{ + private string _key = ""; + private string _description = ""; + private string _target = ""; + private string _type = "app"; + + public string Key { get => _key; set { _key = value; OnPropertyChanged(); } } + public string Description { get => _description; set { _description = value; OnPropertyChanged(); } } + public string Target { get => _target; set { _target = value; OnPropertyChanged(); } } + public string Type { get => _type; set { _type = value; OnPropertyChanged(); OnPropertyChanged(nameof(TypeSymbol)); OnPropertyChanged(nameof(TypeLabel)); } } + + public string TypeSymbol => Type switch + { + "url" => Symbols.Globe, + "folder" => Symbols.Folder, + _ => Symbols.App + }; + + public string TypeLabel => Type switch + { + "url" => "URL", + "folder" => "폴더", + _ => "앱" + }; + + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged([CallerMemberName] string? n = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); +} + +/// +/// 배치 명령 항목 (> 프리픽스) +/// +public class BatchCommandModel : INotifyPropertyChanged +{ + private string _key = ""; + private string _command = ""; + private bool _showWindow; + + public string Key { get => _key; set { _key = value; OnPropertyChanged(); } } + public string Command { get => _command; set { _command = value; OnPropertyChanged(); } } + public bool ShowWindow { get => _showWindow; set { _showWindow = value; OnPropertyChanged(); } } + + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged([CallerMemberName] string? n = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); +} + +// ─── 프롬프트 템플릿 행 모델 ───────────────────────────────────────────────────── + +public class PromptTemplateRow : INotifyPropertyChanged +{ + private string _name = ""; + private string _content = ""; + private string _icon = "\uE8BD"; + + public string Name { get => _name; set { _name = value; OnPropertyChanged(); } } + public string Content { get => _content; set { _content = value; OnPropertyChanged(); OnPropertyChanged(nameof(Preview)); } } + public string Icon { get => _icon; set { _icon = value; OnPropertyChanged(); } } + + public string Preview => Content.Length > 60 + ? Content[..57].Replace("\n", " ") + "…" + : Content.Replace("\n", " "); + + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged([CallerMemberName] string? n = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); +} + +// ─── 등록 모델 행 모델 ────────────────────────────────────────────────────────── + +public class RegisteredModelRow : INotifyPropertyChanged +{ + private string _alias = ""; + private string _encryptedModelName = ""; + private string _service = "ollama"; + private string _endpoint = ""; + private string _apiKey = ""; + + /// UI 표시용 별칭 + public string Alias + { + get => _alias; + set { _alias = value; OnPropertyChanged(); } + } + + /// 암호화된 실제 모델명 (PortableEncrypt) + public string EncryptedModelName + { + get => _encryptedModelName; + set { _encryptedModelName = value; OnPropertyChanged(); OnPropertyChanged(nameof(MaskedModelName)); } + } + + /// 서비스 타입 (ollama / vllm) + public string Service + { + get => _service; + set { _service = value; OnPropertyChanged(); OnPropertyChanged(nameof(ServiceLabel)); } + } + + /// 이 모델 전용 서버 엔드포인트. 비어있으면 기본 엔드포인트 사용. + public string Endpoint + { + get => _endpoint; + set { _endpoint = value; OnPropertyChanged(); OnPropertyChanged(nameof(EndpointDisplay)); } + } + + /// 이 모델 전용 API 키. 비어있으면 기본 API 키 사용. + public string ApiKey + { + get => _apiKey; + set { _apiKey = value; OnPropertyChanged(); } + } + + // ── CP4D 인증 필드 ────────────────────────────────────────────────── + + private string _authType = "bearer"; + private string _cp4dUrl = ""; + private string _cp4dUsername = ""; + private string _cp4dPassword = ""; + + /// 인증 방식. bearer | cp4d + public string AuthType + { + get => _authType; + set { _authType = value; OnPropertyChanged(); OnPropertyChanged(nameof(AuthLabel)); } + } + + /// CP4D 서버 URL + public string Cp4dUrl + { + get => _cp4dUrl; + set { _cp4dUrl = value; OnPropertyChanged(); } + } + + /// CP4D 사용자 이름 + public string Cp4dUsername + { + get => _cp4dUsername; + set { _cp4dUsername = value; OnPropertyChanged(); } + } + + /// CP4D 비밀번호 (암호화 저장) + public string Cp4dPassword + { + get => _cp4dPassword; + set { _cp4dPassword = value; OnPropertyChanged(); } + } + + /// 인증 방식 라벨 + public string AuthLabel => _authType == "cp4d" ? "CP4D" : "Bearer"; + + /// UI에 표시할 엔드포인트 요약 + public string EndpointDisplay => string.IsNullOrEmpty(_endpoint) ? "(기본 서버)" : _endpoint; + + /// UI에 표시할 마스킹된 모델명 + public string MaskedModelName => string.IsNullOrEmpty(_encryptedModelName) ? "(미등록)" : "••••••••"; + + /// 서비스 라벨 + public string ServiceLabel => _service == "vllm" ? "vLLM" : "Ollama"; + + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged([CallerMemberName] string? n = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); +} + +// ─── 스니펫 행 모델 ──────────────────────────────────────────────────────────── + +public class SnippetRowModel : INotifyPropertyChanged +{ + private string _key = ""; + private string _name = ""; + private string _content = ""; + + public string Key { get => _key; set { _key = value; OnPropertyChanged(); } } + public string Name { get => _name; set { _name = value; OnPropertyChanged(); } } + public string Content { get => _content; set { _content = value; OnPropertyChanged(); } } + + public string Preview => Content.Length > 50 + ? Content[..47].Replace("\n", " ") + "…" + : Content.Replace("\n", " "); + + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged([CallerMemberName] string? n = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); +}