Files
AX-Copilot-Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs
lacvet 817fc94f41
Some checks failed
Release Gate / gate (push) Has been cancelled
IBM 연동형 vLLM 인증 실패 원인 수정
IBM Cloud 계열 vLLM 연결에서 등록 모델 인증 방식이 Bearer와 CP4D만 지원하던 문제를 점검하고, IBM IAM 토큰 교환 경로를 추가했습니다.

- RegisteredModel/AuthType에 ibm_iam 경로를 반영했습니다.

- IbmIamTokenService를 추가해 API 키를 IAM access token으로 교환한 뒤 Bearer 헤더로 적용하도록 했습니다.

- 모델 등록 다이얼로그, 설정 ViewModel, AX Agent 오버레이 모델 목록에도 IBM IAM 표시를 추가했습니다.

- README.md와 docs/DEVELOPMENT.md에 2026-04-06 14:06 (KST) 기준 이력을 반영했습니다.

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

2101 lines
83 KiB
C#

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Forms;
using System.Windows.Media;
using AxCopilot.Models;
using AxCopilot.Services;
using AxCopilot.Services.Agent;
using AxCopilot.Themes;
using AxCopilot.Views;
namespace AxCopilot.ViewModels;
/// <summary>
/// 설정 카드에 표시할 테마 미리보기 데이터
/// </summary>
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));
}
/// <summary>
/// 커스텀 테마 단일 색상 항목 (색상 편집 탭 1행 1색)
/// </summary>
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 new SolidColorBrush((Color)ColorConverter.ConvertFromString(Hex)); }
catch { 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));
}
/// <summary>
/// 빠른 실행 단축키 항목 (키워드 → 앱/URL/폴더)
/// </summary>
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));
}
/// <summary>
/// 배치 명령 항목 (> 프리픽스)
/// </summary>
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
{
private readonly SettingsService _service;
internal SettingsService Service => _service;
/// <summary>CodeSettings 바인딩용 프로퍼티. XAML에서 {Binding Code.EnableLsp} 등으로 접근.</summary>
public Models.CodeSettings Code => _service.Settings.Llm.Code;
// ─── 작업 복사본 ───────────────────────────────────────────────────────
private string _hotkey;
private int _maxResults;
private double _opacity;
private string _selectedThemeKey;
private string _launcherPosition;
private string _webSearchEngine;
private bool _snippetAutoExpand;
private string _language;
private string _indexSpeed;
private bool _aiEnabled = true;
private string _operationMode = "internal";
private string _agentTheme = "system";
private string _agentThemePreset = "claw";
// 기능 토글
private bool _showNumberBadges;
private bool _enableFavorites;
private bool _enableRecent;
private bool _enableActionMode;
private bool _closeOnFocusLost;
private bool _rememberPosition;
private bool _showPrefixBadge;
private bool _enableIconAnimation;
private bool _enableRainbowGlow;
private bool _enableSelectionGlow;
private bool _enableRandomPlaceholder;
private bool _showLauncherBorder;
private bool _showWidgetPerf;
private bool _showWidgetPomo;
private bool _showWidgetNote;
private bool _showWidgetWeather;
private bool _showWidgetCalendar;
private bool _showWidgetBattery;
private bool _shortcutHelpUseThemeColor;
// LLM 공통 설정
private string _llmService;
private bool _llmStreaming;
private int _llmMaxContextTokens;
private int _llmRetentionDays;
private double _llmTemperature;
// 서비스별 독립 설정
private string _ollamaEndpoint = "http://localhost:11434";
private string _ollamaApiKey = "";
private string _ollamaModel = "";
private string _vllmEndpoint = "";
private string _vllmApiKey = "";
private string _vllmModel = "";
private bool _vllmAllowInsecureTls;
private string _geminiApiKey = "";
private string _geminiModel = "gemini-2.5-flash";
private string _sigmoidApiKey = "";
private string _sigmoidModel = "cl" + "aude-sonnet-4-6";
// 등록 모델 목록
public ObservableCollection<RegisteredModelRow> RegisteredModels { get; } = new();
private static string NormalizeServiceKey(string? service)
=> (service ?? "ollama").Trim().ToLowerInvariant() switch
{
"sigmoid" => "claude",
_ => (service ?? "ollama").Trim().ToLowerInvariant(),
};
public string LlmService
{
get => _llmService;
set { _llmService = NormalizeServiceKey(value); OnPropertyChanged(); OnPropertyChanged(nameof(IsInternalService)); OnPropertyChanged(nameof(IsExternalService)); OnPropertyChanged(nameof(NeedsEndpoint)); OnPropertyChanged(nameof(NeedsApiKey)); OnPropertyChanged(nameof(IsGeminiSelected)); OnPropertyChanged(nameof(IsClaudeSelected)); OnPropertyChanged(nameof(IsSigmoidSelected)); }
}
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";
public bool IsSigmoidSelected => IsClaudeSelected;
// ── 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(); } }
public bool VllmAllowInsecureTls { get => _vllmAllowInsecureTls; set { _vllmAllowInsecureTls = 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 => _sigmoidApiKey; set { _sigmoidApiKey = value; OnPropertyChanged(); OnPropertyChanged(nameof(SigmoidApiKey)); } }
public string ClaudeModel { get => _sigmoidModel; set { _sigmoidModel = value; OnPropertyChanged(); OnPropertyChanged(nameof(SigmoidModel)); } }
public string SigmoidApiKey { get => ClaudeApiKey; set => ClaudeApiKey = value; } // legacy binding
public string SigmoidModel { get => ClaudeModel; set => ClaudeModel = value; } // legacy binding
// ── 공통 응답 설정 ──
public bool LlmStreaming
{
get => _llmStreaming;
set { _llmStreaming = value; OnPropertyChanged(); }
}
public int LlmMaxContextTokens
{
get => _llmMaxContextTokens;
set { _llmMaxContextTokens = Math.Clamp(value, 1024, 1_000_000); 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 = PermissionModeCatalog.NormalizeGlobalMode(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 bool _enableProactiveContextCompact;
public bool EnableProactiveContextCompact
{
get => _enableProactiveContextCompact;
set { _enableProactiveContextCompact = value; OnPropertyChanged(); }
}
private int _contextCompactTriggerPercent;
public int ContextCompactTriggerPercent
{
get => _contextCompactTriggerPercent;
set { _contextCompactTriggerPercent = Math.Clamp(value, 50, 95); OnPropertyChanged(); }
}
private string _agentLogLevel;
public string AgentLogLevel
{
get => _agentLogLevel;
set { _agentLogLevel = value; OnPropertyChanged(); }
}
private string _agentUiExpressionLevel = "balanced";
public string AgentUiExpressionLevel
{
get => _agentUiExpressionLevel;
set
{
_agentUiExpressionLevel = (value ?? "balanced").Trim().ToLowerInvariant() switch
{
"rich" => "rich",
"simple" => "simple",
_ => "balanced",
};
OnPropertyChanged();
}
}
private int _planDiffSeverityMediumCount = 2;
public int PlanDiffSeverityMediumCount
{
get => _planDiffSeverityMediumCount;
set { _planDiffSeverityMediumCount = Math.Clamp(value, 1, 20); OnPropertyChanged(); }
}
private int _planDiffSeverityHighCount = 5;
public int PlanDiffSeverityHighCount
{
get => _planDiffSeverityHighCount;
set { _planDiffSeverityHighCount = Math.Clamp(value, 1, 30); OnPropertyChanged(); }
}
private int _planDiffSeverityMediumRatioPercent = 25;
public int PlanDiffSeverityMediumRatioPercent
{
get => _planDiffSeverityMediumRatioPercent;
set { _planDiffSeverityMediumRatioPercent = Math.Clamp(value, 1, 100); OnPropertyChanged(); }
}
private int _planDiffSeverityHighRatioPercent = 60;
public int PlanDiffSeverityHighRatioPercent
{
get => _planDiffSeverityHighRatioPercent;
set { _planDiffSeverityHighRatioPercent = Math.Clamp(value, 1, 100); OnPropertyChanged(); }
}
private string _agentDecisionLevel = "detailed";
public string AgentDecisionLevel
{
get => _agentDecisionLevel;
set { _agentDecisionLevel = 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 _enableHookInputMutation;
public bool EnableHookInputMutation
{
get => _enableHookInputMutation;
set { _enableHookInputMutation = value; OnPropertyChanged(); }
}
private bool _enableHookPermissionUpdate;
public bool EnableHookPermissionUpdate
{
get => _enableHookPermissionUpdate;
set { _enableHookPermissionUpdate = value; OnPropertyChanged(); }
}
// ── 스킬 시스템 ──
private bool _enableSkillSystem = true;
public bool EnableSkillSystem
{
get => _enableSkillSystem;
set { _enableSkillSystem = value; OnPropertyChanged(); }
}
private bool _enableForkSkillDelegationEnforcement = true;
public bool EnableForkSkillDelegationEnforcement
{
get => _enableForkSkillDelegationEnforcement;
set { _enableForkSkillDelegationEnforcement = 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(); }
}
private int _maxFavoriteSlashCommands = 10;
public int MaxFavoriteSlashCommands
{
get => _maxFavoriteSlashCommands;
set { _maxFavoriteSlashCommands = Math.Clamp(value, 1, 30); OnPropertyChanged(); }
}
private int _maxRecentSlashCommands = 20;
public int MaxRecentSlashCommands
{
get => _maxRecentSlashCommands;
set { _maxRecentSlashCommands = Math.Clamp(value, 5, 50); 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<string> BlockedPaths { get; } = new();
public ObservableCollection<string> 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 bool AiEnabled
{
get => _aiEnabled;
set { _aiEnabled = value; OnPropertyChanged(); }
}
public string OperationMode
{
get => _operationMode;
set { _operationMode = string.IsNullOrWhiteSpace(value) ? "internal" : value.Trim().ToLowerInvariant(); OnPropertyChanged(); }
}
public string AgentTheme
{
get => _agentTheme;
set { _agentTheme = string.IsNullOrWhiteSpace(value) ? "system" : value.Trim().ToLowerInvariant(); OnPropertyChanged(); }
}
public string AgentThemePreset
{
get => _agentThemePreset;
set { _agentThemePreset = string.IsNullOrWhiteSpace(value) ? "claw" : value.Trim().ToLowerInvariant(); OnPropertyChanged(); }
}
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 RememberPosition
{
get => _rememberPosition;
set { _rememberPosition = value; OnPropertyChanged(); }
}
public bool ShowPrefixBadge
{
get => _showPrefixBadge;
set { _showPrefixBadge = value; OnPropertyChanged(); }
}
public bool ShowWidgetPerf
{
get => _showWidgetPerf;
set { _showWidgetPerf = value; OnPropertyChanged(); }
}
public bool ShowWidgetPomo
{
get => _showWidgetPomo;
set { _showWidgetPomo = value; OnPropertyChanged(); }
}
public bool ShowWidgetNote
{
get => _showWidgetNote;
set { _showWidgetNote = value; OnPropertyChanged(); }
}
public bool ShowWidgetWeather
{
get => _showWidgetWeather;
set { _showWidgetWeather = value; OnPropertyChanged(); }
}
public bool ShowWidgetCalendar
{
get => _showWidgetCalendar;
set { _showWidgetCalendar = value; OnPropertyChanged(); }
}
public bool ShowWidgetBattery
{
get => _showWidgetBattery;
set { _showWidgetBattery = 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<ThemeCardModel> 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<ColorRowModel> ColorRows { get; }
// ─── 프롬프트 템플릿 ──────────────────────────────────────────────────
public ObservableCollection<PromptTemplateRow> PromptTemplates { get; } = new();
// ─── 배치 명령 ────────────────────────────────────────────────────────
public ObservableCollection<BatchCommandModel> 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<AppShortcutModel> 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;
public SettingsViewModel(SettingsService service)
{
_service = service;
var s = service.Settings;
_hotkey = s.Hotkey;
_maxResults = s.Launcher.MaxResults;
_opacity = s.Launcher.Opacity;
_selectedThemeKey = (s.Launcher.Theme ?? "system").ToLowerInvariant();
_launcherPosition = s.Launcher.Position;
_webSearchEngine = s.Launcher.WebSearchEngine;
_snippetAutoExpand = s.Launcher.SnippetAutoExpand;
_language = s.Launcher.Language;
_indexSpeed = s.IndexSpeed ?? "normal";
// 기능 토글 로드
_showNumberBadges = s.Launcher.ShowNumberBadges;
_enableFavorites = s.Launcher.EnableFavorites;
_enableRecent = s.Launcher.EnableRecent;
_enableActionMode = s.Launcher.EnableActionMode;
_closeOnFocusLost = s.Launcher.CloseOnFocusLost;
_rememberPosition = s.Launcher.RememberPosition;
_showPrefixBadge = s.Launcher.ShowPrefixBadge;
_enableIconAnimation = s.Launcher.EnableIconAnimation;
_enableRainbowGlow = s.Launcher.EnableRainbowGlow;
_enableSelectionGlow = s.Launcher.EnableSelectionGlow;
_enableRandomPlaceholder = s.Launcher.EnableRandomPlaceholder;
_showLauncherBorder = s.Launcher.ShowLauncherBorder;
_showWidgetPerf = s.Launcher.ShowWidgetPerf;
_showWidgetPomo = s.Launcher.ShowWidgetPomo;
_showWidgetNote = s.Launcher.ShowWidgetNote;
_showWidgetWeather = s.Launcher.ShowWidgetWeather;
_showWidgetCalendar = s.Launcher.ShowWidgetCalendar;
_showWidgetBattery = s.Launcher.ShowWidgetBattery;
_shortcutHelpUseThemeColor = s.Launcher.ShortcutHelpUseThemeColor;
_enableTextAction = s.Launcher.EnableTextAction;
// v1.7.1: 파일 대화상자 통합 기본값을 false로 변경 (브라우저 업로드 오작동 방지)
// 기존 저장값이 true이면 false로 재설정
_enableFileDialogIntegration = false;
s.Launcher.EnableFileDialogIntegration = false;
_enableClipboardAutoCategory = s.Launcher.EnableClipboardAutoCategory;
_maxPinnedClipboardItems = s.Launcher.MaxPinnedClipboardItems;
_textActionTranslateLanguage = s.Launcher.TextActionTranslateLanguage;
_maxSubAgents = s.Llm.MaxSubAgents;
_pdfExportPath = s.Llm.PdfExportPath;
_tipDurationSeconds = s.Llm.TipDurationSeconds;
// LLM 설정 로드
var llm = s.Llm;
_llmService = NormalizeServiceKey(llm.Service);
_llmStreaming = llm.Streaming;
_llmMaxContextTokens = Math.Clamp(llm.MaxContextTokens, 1024, 1_000_000);
_llmRetentionDays = llm.RetentionDays;
_llmTemperature = llm.Temperature;
_defaultAgentPermission = PermissionModeCatalog.NormalizeGlobalMode(llm.DefaultAgentPermission);
_defaultOutputFormat = llm.DefaultOutputFormat;
_defaultMood = string.IsNullOrEmpty(llm.DefaultMood) ? "modern" : llm.DefaultMood;
_autoPreview = llm.AutoPreview;
_maxAgentIterations = llm.MaxAgentIterations > 0 ? llm.MaxAgentIterations : 25;
_maxRetryOnError = llm.MaxRetryOnError > 0 ? llm.MaxRetryOnError : 3;
_enableProactiveContextCompact = llm.EnableProactiveContextCompact;
_contextCompactTriggerPercent = llm.ContextCompactTriggerPercent > 0
? Math.Clamp(llm.ContextCompactTriggerPercent, 50, 95)
: 80;
_aiEnabled = _service.Settings.AiEnabled;
_operationMode = string.IsNullOrWhiteSpace(_service.Settings.OperationMode) ? "internal" : _service.Settings.OperationMode;
_agentTheme = string.IsNullOrWhiteSpace(llm.AgentTheme) ? "system" : llm.AgentTheme;
_agentThemePreset = string.IsNullOrWhiteSpace(llm.AgentThemePreset) ? "claw" : llm.AgentThemePreset;
_agentLogLevel = llm.AgentLogLevel;
_agentUiExpressionLevel = (llm.AgentUiExpressionLevel ?? "balanced").Trim().ToLowerInvariant() switch
{
"rich" => "rich",
"simple" => "simple",
_ => "balanced",
};
_planDiffSeverityMediumCount = llm.PlanDiffSeverityMediumCount > 0 ? Math.Clamp(llm.PlanDiffSeverityMediumCount, 1, 20) : 2;
_planDiffSeverityHighCount = llm.PlanDiffSeverityHighCount > 0 ? Math.Clamp(llm.PlanDiffSeverityHighCount, 1, 30) : 5;
_planDiffSeverityMediumRatioPercent = llm.PlanDiffSeverityMediumRatioPercent > 0 ? Math.Clamp(llm.PlanDiffSeverityMediumRatioPercent, 1, 100) : 25;
_planDiffSeverityHighRatioPercent = llm.PlanDiffSeverityHighRatioPercent > 0 ? Math.Clamp(llm.PlanDiffSeverityHighRatioPercent, 1, 100) : 60;
_agentDecisionLevel = llm.AgentDecisionLevel;
_enableMultiPassDocument = llm.EnableMultiPassDocument;
_enableCoworkVerification = llm.EnableCoworkVerification;
_enableFilePathHighlight = llm.EnableFilePathHighlight;
_folderDataUsage = string.IsNullOrEmpty(llm.FolderDataUsage) ? "active" : llm.FolderDataUsage;
_enableAuditLog = llm.EnableAuditLog;
_enableAgentMemory = llm.EnableAgentMemory;
_enableProjectRules = llm.EnableProjectRules;
_maxMemoryEntries = llm.MaxMemoryEntries;
_enableImageInput = llm.EnableImageInput;
_maxImageSizeKb = llm.MaxImageSizeKb > 0 ? llm.MaxImageSizeKb : 5120;
_enableToolHooks = llm.EnableToolHooks;
_toolHookTimeoutMs = llm.ToolHookTimeoutMs > 0 ? llm.ToolHookTimeoutMs : 10000;
_enableHookInputMutation = llm.EnableHookInputMutation;
_enableHookPermissionUpdate = llm.EnableHookPermissionUpdate;
_enableSkillSystem = llm.EnableSkillSystem;
_enableForkSkillDelegationEnforcement = llm.EnableForkSkillDelegationEnforcement;
_skillsFolderPath = llm.SkillsFolderPath;
_slashPopupPageSize = llm.SlashPopupPageSize > 0 ? Math.Clamp(llm.SlashPopupPageSize, 3, 10) : 6;
_maxFavoriteSlashCommands = llm.MaxFavoriteSlashCommands > 0 ? Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30) : 10;
_maxRecentSlashCommands = llm.MaxRecentSlashCommands > 0 ? Math.Clamp(llm.MaxRecentSlashCommands, 5, 50) : 20;
_enableDragDropAiActions = llm.EnableDragDropAiActions;
_dragDropAutoSend = llm.DragDropAutoSend;
_enableCodeReview = llm.Code.EnableCodeReview;
_enableAutoRouter = llm.EnableAutoRouter;
_autoRouterConfidence = llm.AutoRouterConfidence;
_enableChatRainbowGlow = llm.EnableChatRainbowGlow;
_notifyOnComplete = llm.NotifyOnComplete;
_showTips = llm.ShowTips;
_devMode = llm.DevMode; // 저장된 설정 유지 (한 번 켜면 유지, 끄면 하위 기능 비활성)
_devModeStepApproval = llm.DevModeStepApproval;
_workflowVisualizer = llm.WorkflowVisualizer;
_freeTierMode = llm.FreeTierMode;
_freeTierDelaySeconds = llm.FreeTierDelaySeconds > 0 ? llm.FreeTierDelaySeconds : 4;
_showTotalCallStats = llm.ShowTotalCallStats;
// 서비스별 독립 설정 로드
_ollamaEndpoint = llm.OllamaEndpoint;
_ollamaModel = llm.OllamaModel;
_vllmEndpoint = llm.VllmEndpoint;
_vllmModel = llm.VllmModel;
_vllmAllowInsecureTls = llm.VllmAllowInsecureTls;
_geminiModel = string.IsNullOrEmpty(llm.GeminiModel) ? "gemini-2.5-flash" : llm.GeminiModel;
_sigmoidModel = string.IsNullOrEmpty(llm.ClaudeModel) ? "cl" + "aude-sonnet-4-6" : llm.ClaudeModel;
// API 키 로드: 서비스별 독립 저장
_geminiApiKey = llm.GeminiApiKey;
_sigmoidApiKey = llm.ClaudeApiKey;
if (llm.EncryptionEnabled)
{
_ollamaApiKey = string.IsNullOrEmpty(llm.OllamaApiKey) ? "" : "(저장됨)";
_vllmApiKey = string.IsNullOrEmpty(llm.VllmApiKey) ? "" : "(저장됨)";
}
else
{
_ollamaApiKey = llm.OllamaApiKey;
_vllmApiKey = llm.VllmApiKey;
}
// 기존 단일 설정에서 마이그레이션 (최초 1회)
if (string.IsNullOrEmpty(llm.OllamaEndpoint) && !string.IsNullOrEmpty(llm.Endpoint) && llm.Service == "ollama")
{
_ollamaEndpoint = llm.Endpoint;
_ollamaModel = llm.Model;
if (!llm.EncryptionEnabled) _ollamaApiKey = llm.EncryptedApiKey;
}
if (string.IsNullOrEmpty(llm.VllmEndpoint) && !string.IsNullOrEmpty(llm.Endpoint) && llm.Service == "vllm")
{
_vllmEndpoint = llm.Endpoint;
_vllmModel = llm.Model;
if (!llm.EncryptionEnabled) _vllmApiKey = llm.EncryptedApiKey;
}
if (string.IsNullOrEmpty(llm.GeminiApiKey) && !string.IsNullOrEmpty(llm.ApiKey) && llm.Service == "gemini")
{
_geminiApiKey = llm.ApiKey;
_geminiModel = llm.Model;
}
if (string.IsNullOrEmpty(llm.ClaudeApiKey) && !string.IsNullOrEmpty(llm.ApiKey) && NormalizeServiceKey(llm.Service) == "claude")
{
_sigmoidApiKey = llm.ApiKey;
_sigmoidModel = llm.Model;
}
// 차단 경로/확장자 로드
foreach (var p in llm.BlockedPaths) BlockedPaths.Add(p);
foreach (var e in llm.BlockedExtensions) BlockedExtensions.Add(e);
// 등록 모델 로드
foreach (var rm in llm.RegisteredModels)
RegisteredModels.Add(new RegisteredModelRow
{
Alias = rm.Alias,
EncryptedModelName = rm.EncryptedModelName,
Service = rm.Service,
Endpoint = rm.Endpoint,
ApiKey = rm.ApiKey,
AllowInsecureTls = rm.AllowInsecureTls,
AuthType = rm.AuthType ?? "bearer",
Cp4dUrl = rm.Cp4dUrl ?? "",
Cp4dUsername = rm.Cp4dUsername ?? "",
Cp4dPassword = rm.Cp4dPassword ?? "",
});
// 프롬프트 템플릿 로드
foreach (var pt in llm.PromptTemplates)
PromptTemplates.Add(new PromptTemplateRow { Name = pt.Name, Content = pt.Content, Icon = pt.Icon });
foreach (var card in ThemeCards)
card.IsSelected = card.Key == _selectedThemeKey;
// 알림 설정 로드
_reminderEnabled = s.Reminder.Enabled;
_reminderCorner = s.Reminder.Corner;
_reminderIntervalMinutes = s.Reminder.IntervalMinutes;
_reminderDisplaySeconds = s.Reminder.DisplaySeconds;
// 캡처 설정 로드
_capPrefix = string.IsNullOrWhiteSpace(s.ScreenCapture.Prefix) ? "cap" : s.ScreenCapture.Prefix;
_capGlobalHotkeyEnabled = s.ScreenCapture.GlobalHotkeyEnabled;
_capGlobalHotkey = string.IsNullOrWhiteSpace(s.ScreenCapture.GlobalHotkey) ? "PrintScreen" : s.ScreenCapture.GlobalHotkey;
_capGlobalMode = string.IsNullOrWhiteSpace(s.ScreenCapture.GlobalHotkeyMode) ? "screen" : s.ScreenCapture.GlobalHotkeyMode;
_capScrollDelayMs = s.ScreenCapture.ScrollDelayMs > 0 ? s.ScreenCapture.ScrollDelayMs : 120;
// 클립보드 히스토리 설정 로드
_clipboardEnabled = s.ClipboardHistory.Enabled;
_clipboardMaxItems = s.ClipboardHistory.MaxItems;
// 시스템 명령 설정 로드
var sys = s.SystemCommands;
_sysShowLock = sys.ShowLock;
_sysShowSleep = sys.ShowSleep;
_sysShowRestart = sys.ShowRestart;
_sysShowShutdown = sys.ShowShutdown;
_sysShowHibernate = sys.ShowHibernate;
_sysShowLogout = sys.ShowLogout;
_sysShowRecycleBin = sys.ShowRecycleBin;
// 시스템 명령 별칭 로드
var ca = sys.CommandAliases;
_aliasLock = FormatAliases(ca, "lock");
_aliasSleep = FormatAliases(ca, "sleep");
_aliasRestart = FormatAliases(ca, "restart");
_aliasShutdown = FormatAliases(ca, "shutdown");
_aliasHibernate = FormatAliases(ca, "hibernate");
_aliasLogout = FormatAliases(ca, "logout");
_aliasRecycle = FormatAliases(ca, "recycle");
// 인덱스 경로 로드
foreach (var path in s.IndexPaths)
IndexPaths.Add(path);
// 인덱스 확장자 로드
foreach (var ext in s.IndexExtensions)
IndexExtensions.Add(ext);
// 스니펫 로드
foreach (var sn in s.Snippets)
Snippets.Add(new SnippetRowModel { Key = sn.Key, Name = sn.Name, Content = sn.Content });
// 배치 명령 로드
foreach (var alias in s.Aliases.Where(a => a.Type == "batch"))
{
BatchCommands.Add(new BatchCommandModel
{
Key = alias.Key,
Command = alias.Target,
ShowWindow = alias.ShowWindow
});
}
// 기존 aliases 로드 (app/url/folder type만)
foreach (var alias in s.Aliases.Where(a => a.Type is "app" or "url" or "folder"))
{
AppShortcuts.Add(new AppShortcutModel
{
Key = alias.Key,
Description = alias.Description ?? "",
Target = alias.Target,
Type = alias.Type
});
}
var c = s.Launcher.CustomTheme ?? new CustomThemeColors();
ColorRows = new List<ColorRowModel>
{
new("런처 배경", nameof(CustomThemeColors.LauncherBackground), c.LauncherBackground),
new("항목 배경", nameof(CustomThemeColors.ItemBackground), c.ItemBackground),
new("선택 항목 배경", nameof(CustomThemeColors.ItemSelectedBackground), c.ItemSelectedBackground),
new("호버 배경", nameof(CustomThemeColors.ItemHoverBackground), c.ItemHoverBackground),
new("기본 텍스트", nameof(CustomThemeColors.PrimaryText), c.PrimaryText),
new("보조 텍스트", nameof(CustomThemeColors.SecondaryText), c.SecondaryText),
new("플레이스홀더", nameof(CustomThemeColors.PlaceholderText), c.PlaceholderText),
new("강조색", nameof(CustomThemeColors.AccentColor), c.AccentColor),
new("구분선", nameof(CustomThemeColors.SeparatorColor), c.SeparatorColor),
new("힌트 배경", nameof(CustomThemeColors.HintBackground), c.HintBackground),
new("힌트 텍스트", nameof(CustomThemeColors.HintText), c.HintText),
new("테두리", nameof(CustomThemeColors.BorderColor), c.BorderColor),
new("스크롤바", nameof(CustomThemeColors.ScrollbarThumb), c.ScrollbarThumb),
new("그림자", nameof(CustomThemeColors.ShadowColor), c.ShadowColor),
};
_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<string> 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<string> 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);
/// <summary>파일 선택 대화상자. 선택 시 NewTarget에 자동 설정.</summary>
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 = (Color)ColorConverter.ConvertFromString(row.Hex);
dlg.Color = System.Drawing.Color.FromArgb(color.R, color.G, color.B);
}
catch { /* 기본값 사용 */ }
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<string> 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",
};
/// <summary>설정 저장 전 프리픽스/키워드 충돌을 검사합니다. 충돌 시 메시지를 반환합니다.</summary>
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.RememberPosition = _rememberPosition;
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.ShowWidgetPerf = _showWidgetPerf;
s.Launcher.ShowWidgetPomo = _showWidgetPomo;
s.Launcher.ShowWidgetNote = _showWidgetNote;
s.Launcher.ShowWidgetWeather = _showWidgetWeather;
s.Launcher.ShowWidgetCalendar = _showWidgetCalendar;
s.Launcher.ShowWidgetBattery = _showWidgetBattery;
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 = Math.Clamp(_llmMaxContextTokens, 1024, 1_000_000);
s.Llm.RetentionDays = _llmRetentionDays;
s.Llm.Temperature = _llmTemperature;
s.Llm.DefaultAgentPermission = PermissionModeCatalog.NormalizeGlobalMode(_defaultAgentPermission);
s.Llm.DefaultOutputFormat = _defaultOutputFormat;
s.Llm.DefaultMood = _defaultMood;
s.Llm.AutoPreview = _autoPreview;
s.Llm.MaxAgentIterations = _maxAgentIterations;
s.Llm.MaxRetryOnError = _maxRetryOnError;
s.Llm.EnableProactiveContextCompact = _enableProactiveContextCompact;
s.Llm.ContextCompactTriggerPercent = _contextCompactTriggerPercent;
s.AiEnabled = _aiEnabled;
s.OperationMode = _operationMode;
s.Llm.AgentTheme = _agentTheme;
s.Llm.AgentThemePreset = _agentThemePreset;
s.Llm.AgentLogLevel = _agentLogLevel;
s.Llm.AgentUiExpressionLevel = _agentUiExpressionLevel;
s.Llm.PlanDiffSeverityMediumCount = _planDiffSeverityMediumCount;
s.Llm.PlanDiffSeverityHighCount = _planDiffSeverityHighCount;
s.Llm.PlanDiffSeverityMediumRatioPercent = _planDiffSeverityMediumRatioPercent;
s.Llm.PlanDiffSeverityHighRatioPercent = _planDiffSeverityHighRatioPercent;
s.Llm.AgentDecisionLevel = _agentDecisionLevel;
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.EnableHookInputMutation = _enableHookInputMutation;
s.Llm.EnableHookPermissionUpdate = _enableHookPermissionUpdate;
s.Llm.EnableSkillSystem = _enableSkillSystem;
s.Llm.EnableForkSkillDelegationEnforcement = _enableForkSkillDelegationEnforcement;
s.Llm.SkillsFolderPath = _skillsFolderPath;
s.Llm.SlashPopupPageSize = _slashPopupPageSize;
s.Llm.MaxFavoriteSlashCommands = _maxFavoriteSlashCommands;
s.Llm.MaxRecentSlashCommands = _maxRecentSlashCommands;
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.VllmAllowInsecureTls = _vllmAllowInsecureTls;
s.Llm.GeminiModel = _geminiModel;
s.Llm.ClaudeModel = _sigmoidModel;
s.Llm.GeminiApiKey = _geminiApiKey;
s.Llm.ClaudeApiKey = _sigmoidApiKey;
// 내부 서비스 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":
case "sigmoid":
s.Llm.ApiKey = _sigmoidApiKey;
s.Llm.Model = _sigmoidModel;
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,
AllowInsecureTls = rm.AllowInsecureTls,
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<string, List<string>>();
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<SnippetRowModel> 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)); }
}
/// <summary>ComboBox SelectedValue와 string 바인딩 용도</summary>
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<string> 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<string, List<string>> dict, string key)
=> dict.TryGetValue(key, out var list) ? string.Join(", ", list) : "";
private static List<string> 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 = "";
private bool _allowInsecureTls;
/// <summary>UI 표시용 별칭</summary>
public string Alias
{
get => _alias;
set { _alias = value; OnPropertyChanged(); }
}
/// <summary>암호화된 실제 모델명 (PortableEncrypt)</summary>
public string EncryptedModelName
{
get => _encryptedModelName;
set { _encryptedModelName = value; OnPropertyChanged(); OnPropertyChanged(nameof(MaskedModelName)); }
}
/// <summary>서비스 타입 (ollama / vllm)</summary>
public string Service
{
get => _service;
set { _service = value; OnPropertyChanged(); OnPropertyChanged(nameof(ServiceLabel)); }
}
/// <summary>이 모델 전용 서버 엔드포인트. 비어있으면 기본 엔드포인트 사용.</summary>
public string Endpoint
{
get => _endpoint;
set { _endpoint = value; OnPropertyChanged(); OnPropertyChanged(nameof(EndpointDisplay)); }
}
/// <summary>이 모델 전용 API 키. 비어있으면 기본 API 키 사용.</summary>
public string ApiKey
{
get => _apiKey;
set { _apiKey = value; OnPropertyChanged(); }
}
/// <summary>이 모델 요청 시 TLS 인증서 검증을 생략합니다.</summary>
public bool AllowInsecureTls
{
get => _allowInsecureTls;
set { _allowInsecureTls = value; OnPropertyChanged(); }
}
// ── CP4D 인증 필드 ──────────────────────────────────────────────────
private string _authType = "bearer";
private string _cp4dUrl = "";
private string _cp4dUsername = "";
private string _cp4dPassword = "";
/// <summary>인증 방식. bearer | ibm_iam | cp4d</summary>
public string AuthType
{
get => _authType;
set { _authType = value; OnPropertyChanged(); OnPropertyChanged(nameof(AuthLabel)); }
}
/// <summary>CP4D 서버 URL</summary>
public string Cp4dUrl
{
get => _cp4dUrl;
set { _cp4dUrl = value; OnPropertyChanged(); }
}
/// <summary>CP4D 사용자 이름</summary>
public string Cp4dUsername
{
get => _cp4dUsername;
set { _cp4dUsername = value; OnPropertyChanged(); }
}
/// <summary>CP4D 비밀번호 (암호화 저장)</summary>
public string Cp4dPassword
{
get => _cp4dPassword;
set { _cp4dPassword = value; OnPropertyChanged(); }
}
/// <summary>인증 방식 라벨</summary>
public string AuthLabel => (_authType ?? "bearer").ToLowerInvariant() switch
{
"cp4d" => "CP4D",
"ibm_iam" => "IBM IAM",
_ => "Bearer",
};
/// <summary>UI에 표시할 엔드포인트 요약</summary>
public string EndpointDisplay => string.IsNullOrEmpty(_endpoint) ? "(기본 서버)" : _endpoint;
/// <summary>UI에 표시할 마스킹된 모델명</summary>
public string MaskedModelName => string.IsNullOrEmpty(_encryptedModelName) ? "(미등록)" : "••••••••";
/// <summary>서비스 라벨</summary>
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));
}