프로젝트 상위 경로의 .claude/skills 탐색, 플러그인 스킬 폴더, 보조 스킬 폴더 목록, .claude/commands 기반 레거시 markdown command를 함께 로드하도록 SkillService를 확장했다. 파일형 스킬은 lazy prompt body 캐시를 사용해 실제 호출/미리보기 시점에만 본문을 읽도록 정리했고 arguments + argument-hint를 함께 해석해 위치 인자 치환과 누락 인자 안내를 보강했다. 도구 blanket deny 규칙은 AgentToolCatalog 공통 메서드로 이동해 AgentLoopService와 설정 UI 도구 목록이 같은 노출 정책을 공유하도록 맞췄다. 일반 설정과 AX Agent 설정에는 여러 공용 스킬 폴더를 줄 단위로 연결할 수 있는 additionalSkillFolders 입력을 추가했고 스킬 목록은 번들/프로젝트/플러그인/공용/레거시 source scope별로 더 세분화했다. 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase3\\ -p:IntermediateOutputPath=obj\\verify_phase3\\ (경고 0 / 오류 0) 검증: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolCatalogTests|SkillServiceRuntimePolicyTests" -p:OutputPath=bin\\verify_phase3_tests\\ -p:IntermediateOutputPath=obj\\verify_phase3_tests\\ (통과 18, 기존 WorkspaceContextGeneratorTests nullable 경고 1건 유지)
3778 lines
163 KiB
C#
3778 lines
163 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Windows.Threading;
|
|
using Microsoft.Win32;
|
|
using AxCopilot.Models;
|
|
using AxCopilot.Services;
|
|
using AxCopilot.Services.Agent;
|
|
|
|
namespace AxCopilot.Views;
|
|
|
|
public partial class ChatWindow
|
|
{
|
|
// ─── 모델 전환 ──────────────────────────────────────────────────────
|
|
|
|
// Gemini/Claude 사전 정의 모델 목록
|
|
private static readonly (string Id, string Label)[] GeminiModels =
|
|
{
|
|
("gemini-2.5-pro", "Gemini 2.5 Pro"),
|
|
("gemini-2.5-flash", "Gemini 2.5 Flash"),
|
|
("gemini-2.5-flash-lite", "Gemini 2.5 Flash Lite"),
|
|
("gemini-2.0-flash", "Gemini 2.0 Flash"),
|
|
("gemini-2.0-flash-lite", "Gemini 2.0 Flash Lite"),
|
|
};
|
|
private static readonly (string Id, string Label)[] ClaudeModels =
|
|
{
|
|
(string.Concat("cl", "aude-opus-4-6"), "Claude Opus 4.6"),
|
|
(string.Concat("cl", "aude-sonnet-4-6"), "Claude Sonnet 4.6"),
|
|
(string.Concat("cl", "aude-haiku-4-5-20251001"), "Claude Haiku 4.5"),
|
|
(string.Concat("cl", "aude-sonnet-4-5-20250929"), "Claude Sonnet 4.5"),
|
|
(string.Concat("cl", "aude-opus-4-20250514"), "Claude Opus 4"),
|
|
};
|
|
|
|
/// <summary>현재 선택된 모델의 표시명을 반환합니다.</summary>
|
|
private string GetCurrentModelDisplayName()
|
|
{
|
|
var llm = _settings.Settings.Llm;
|
|
var service = llm.Service.ToLowerInvariant();
|
|
|
|
if (service is "ollama" or "vllm")
|
|
{
|
|
// 등록 모델에서 별칭 찾기
|
|
var registered = llm.RegisteredModels
|
|
.FirstOrDefault(rm => rm.EncryptedModelName == llm.Model);
|
|
if (registered != null) return registered.Alias;
|
|
return string.IsNullOrEmpty(llm.Model) ? "(미설정)" : "••••";
|
|
}
|
|
|
|
if (service == "gemini")
|
|
{
|
|
var m = GeminiModels.FirstOrDefault(g => g.Id == llm.Model);
|
|
return m.Label ?? llm.Model;
|
|
}
|
|
if (service is "sigmoid" or "cl" + "aude")
|
|
{
|
|
var m = ClaudeModels.FirstOrDefault(c => c.Id == llm.Model);
|
|
return m.Label ?? llm.Model;
|
|
}
|
|
return string.IsNullOrEmpty(llm.Model) ? "(미설정)" : llm.Model;
|
|
}
|
|
|
|
private void UpdateModelLabel()
|
|
{
|
|
var service = _settings.Settings.Llm.Service.ToLowerInvariant();
|
|
var serviceLabel = service switch
|
|
{
|
|
"gemini" => "Gemini",
|
|
"sigmoid" or "cl" + "aude" => "Claude",
|
|
"vllm" => "vLLM",
|
|
_ => "Ollama",
|
|
};
|
|
var model = GetCurrentModelDisplayName();
|
|
const int maxLen = 24;
|
|
if (model.Length > maxLen)
|
|
model = model[..(maxLen - 1)] + "…";
|
|
ModelLabel.Text = string.IsNullOrWhiteSpace(model) ? serviceLabel : model;
|
|
if (BtnModelSelector != null)
|
|
BtnModelSelector.ToolTip = $"현재 모델: {GetCurrentModelDisplayName()}\n서비스: {serviceLabel}";
|
|
}
|
|
|
|
private static string NextReasoning(string current) => (current ?? "normal").ToLowerInvariant() switch
|
|
{
|
|
"minimal" => "normal",
|
|
"normal" => "detailed",
|
|
_ => "minimal",
|
|
};
|
|
private static string ReasoningLabel(string value) => (value ?? "normal").ToLowerInvariant() switch
|
|
{
|
|
"minimal" => "낮음",
|
|
"detailed" => "높음",
|
|
_ => "중간",
|
|
};
|
|
private static string NextPermission(string current) => PermissionModeCatalog.NormalizeGlobalMode(current).ToLowerInvariant() switch
|
|
{
|
|
"deny" => "Default",
|
|
"default" => "AcceptEdits",
|
|
"acceptedits" => "Plan",
|
|
"plan" => "BypassPermissions",
|
|
"bypasspermissions" => "Default",
|
|
_ => "Default",
|
|
};
|
|
private static string ServiceLabel(string service) => (service ?? "").ToLowerInvariant() switch
|
|
{
|
|
"gemini" => "Gemini",
|
|
"sigmoid" or "cl" + "aude" => "Claude",
|
|
"vllm" => "vLLM",
|
|
_ => "Ollama",
|
|
};
|
|
|
|
private List<(string Id, string Label)> GetModelCandidates(string service)
|
|
{
|
|
var llm = _settings.Settings.Llm;
|
|
var normalized = (service ?? "ollama").ToLowerInvariant();
|
|
|
|
if (normalized is "ollama" or "vllm")
|
|
{
|
|
return llm.RegisteredModels
|
|
.Where(rm => string.Equals(rm.Service, normalized, StringComparison.OrdinalIgnoreCase))
|
|
.Select(rm => (rm.EncryptedModelName, rm.Alias))
|
|
.ToList();
|
|
}
|
|
|
|
if (normalized == "gemini")
|
|
return GeminiModels.Select(m => (m.Id, m.Label)).ToList();
|
|
if (normalized is "sigmoid" or "cl" + "aude")
|
|
return ClaudeModels.Select(m => (m.Id, m.Label)).ToList();
|
|
|
|
return [];
|
|
}
|
|
|
|
private static bool SupportsOverlayRegisteredModels(string service)
|
|
=> string.Equals(service, "ollama", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(service, "vllm", StringComparison.OrdinalIgnoreCase);
|
|
|
|
private void RefreshInlineSettingsPanel()
|
|
{
|
|
if (InlineSettingsPanel == null) return;
|
|
|
|
var llm = _settings.Settings.Llm;
|
|
var service = (llm.Service ?? "ollama").ToLowerInvariant();
|
|
var models = GetModelCandidates(service);
|
|
|
|
_isInlineSettingsSyncing = true;
|
|
try
|
|
{
|
|
if (InlineServiceCardPanel != null)
|
|
InlineServiceCardPanel.Children.Clear();
|
|
if (InlineModelListPanel != null)
|
|
InlineModelListPanel.Children.Clear();
|
|
|
|
CmbInlineService.Items.Clear();
|
|
foreach (var svc in new[] { "ollama", "vllm", "gemini", "claude" })
|
|
{
|
|
CmbInlineService.Items.Add(new ComboBoxItem
|
|
{
|
|
Content = ServiceLabel(svc),
|
|
Tag = svc,
|
|
});
|
|
}
|
|
var normalizedService = service == "sigmoid" ? string.Concat("cl", "aude") : service;
|
|
CmbInlineService.SelectedIndex = Math.Max(0, new[] { "ollama", "vllm", "gemini", "claude" }.ToList().IndexOf(normalizedService));
|
|
|
|
BuildInlineServiceCards(normalizedService);
|
|
|
|
CmbInlineModel.Items.Clear();
|
|
if (models.Count == 0)
|
|
{
|
|
CmbInlineModel.Items.Add(new ComboBoxItem { Content = "등록된 모델 없음", IsEnabled = false });
|
|
CmbInlineModel.SelectedIndex = 0;
|
|
}
|
|
else
|
|
{
|
|
foreach (var (id, label) in models)
|
|
{
|
|
CmbInlineModel.Items.Add(new ComboBoxItem
|
|
{
|
|
Content = label,
|
|
Tag = id,
|
|
});
|
|
}
|
|
|
|
var selectedIndex = models.FindIndex(m => m.Id == llm.Model);
|
|
CmbInlineModel.SelectedIndex = selectedIndex >= 0 ? selectedIndex : 0;
|
|
BuildInlineModelRows(models, llm.Model);
|
|
}
|
|
|
|
BtnInlineFastMode.Content = GetQuickActionLabel("Gemini 대기", llm.FreeTierMode ? "켜짐" : "꺼짐");
|
|
BtnInlineReasoning.Content = GetQuickActionLabel("추론", ReasoningLabel(llm.AgentDecisionLevel));
|
|
BtnInlinePermission.Content = GetQuickActionLabel("권한", PermissionModeCatalog.ToDisplayLabel(llm.FilePermission));
|
|
BtnInlineSkill.Content = $"스킬 · {(llm.EnableSkillSystem ? "On" : "Off")}";
|
|
BtnInlineCommandBrowser.Content = "명령/스킬 브라우저";
|
|
|
|
var mcpTotal = llm.McpServers?.Count ?? 0;
|
|
var mcpEnabled = llm.McpServers?.Count(x => x.Enabled) ?? 0;
|
|
BtnInlineMcp.Content = $"MCP 상태 · {mcpEnabled}/{mcpTotal}";
|
|
|
|
ApplyQuickActionVisual(BtnInlineFastMode, llm.FreeTierMode, "#ECFDF5", "#166534");
|
|
ApplyQuickActionVisual(BtnInlineReasoning, !string.Equals(llm.AgentDecisionLevel, "normal", StringComparison.OrdinalIgnoreCase), "#EEF2FF", "#1D4ED8");
|
|
ApplyQuickActionVisual(BtnInlinePermission,
|
|
!string.Equals(PermissionModeCatalog.NormalizeGlobalMode(llm.FilePermission), PermissionModeCatalog.Deny, StringComparison.OrdinalIgnoreCase),
|
|
"#FFF7ED",
|
|
"#C2410C");
|
|
}
|
|
finally
|
|
{
|
|
_isInlineSettingsSyncing = false;
|
|
}
|
|
}
|
|
|
|
private void BuildInlineServiceCards(string selectedService)
|
|
{
|
|
if (InlineServiceCardPanel == null)
|
|
return;
|
|
|
|
// 테마 색상 미리 로드 — 하드코딩된 라이트 전용 색상 대신 테마 리소스 사용
|
|
var hintBg = TryFindResource("HintBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0xEA, 0xEA, 0xF0));
|
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
|
var borderColor = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
|
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? hintBg;
|
|
|
|
foreach (var svc in new[] { "ollama", "vllm", "gemini", "claude" })
|
|
{
|
|
var isActive = string.Equals(svc, selectedService, StringComparison.OrdinalIgnoreCase);
|
|
var card = new Border
|
|
{
|
|
Background = isActive ? hintBg : Brushes.Transparent,
|
|
BorderBrush = isActive ? accentBrush : borderColor,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(14),
|
|
Padding = new Thickness(10, 6, 10, 6),
|
|
Margin = new Thickness(0, 0, 6, 6),
|
|
Cursor = Cursors.Hand,
|
|
};
|
|
var text = new TextBlock
|
|
{
|
|
Text = ServiceLabel(svc),
|
|
FontSize = 10.5,
|
|
Foreground = isActive ? accentBrush : primaryText,
|
|
};
|
|
card.Child = text;
|
|
var capturedService = svc;
|
|
card.MouseEnter += (_, _) =>
|
|
{
|
|
if (!isActive)
|
|
card.Background = hoverBg;
|
|
};
|
|
card.MouseLeave += (_, _) =>
|
|
{
|
|
if (!isActive)
|
|
card.Background = Brushes.Transparent;
|
|
};
|
|
card.MouseLeftButtonUp += (_, _) =>
|
|
{
|
|
if (_isInlineSettingsSyncing)
|
|
return;
|
|
_settings.Settings.Llm.Service = capturedService;
|
|
ScheduleSettingsSave();
|
|
UpdateModelLabel();
|
|
RefreshInlineSettingsPanel();
|
|
};
|
|
InlineServiceCardPanel.Children.Add(card);
|
|
}
|
|
}
|
|
|
|
private void BuildInlineModelRows(List<(string Id, string Label)> models, string? selectedModel)
|
|
{
|
|
if (InlineModelListPanel == null)
|
|
return;
|
|
|
|
// 테마 색상 미리 로드 — 하드코딩된 라이트 전용 색상 대신 테마 리소스 사용
|
|
// (다크 테마에서 흰 배경 + 흰 텍스트로 글자가 안보이는 문제 수정)
|
|
var hintBg = TryFindResource("HintBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0xEA, 0xEA, 0xF0));
|
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
|
var borderColor = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
|
|
foreach (var (id, label) in models.Take(8))
|
|
{
|
|
var isActive = string.Equals(id, selectedModel, StringComparison.OrdinalIgnoreCase);
|
|
var row = new Border
|
|
{
|
|
Background = isActive ? hintBg : Brushes.Transparent,
|
|
BorderBrush = isActive ? accentBrush : borderColor,
|
|
BorderThickness = new Thickness(isActive ? 2 : 0, 0, 0, 1),
|
|
Padding = new Thickness(8, 8, 8, 8),
|
|
Cursor = Cursors.Hand,
|
|
};
|
|
var grid = new Grid();
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
|
var labelText = new TextBlock
|
|
{
|
|
Text = label,
|
|
FontSize = 11.5,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = primaryText,
|
|
};
|
|
grid.Children.Add(labelText);
|
|
var stateText = new TextBlock
|
|
{
|
|
Text = isActive ? "사용 중" : "선택",
|
|
FontSize = 10,
|
|
Foreground = isActive ? accentBrush : secondaryText,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
};
|
|
Grid.SetColumn(stateText, 1);
|
|
grid.Children.Add(stateText);
|
|
row.Child = grid;
|
|
var capturedId = id;
|
|
var capturedLabel = label;
|
|
row.MouseEnter += (_, _) =>
|
|
{
|
|
row.Background = hintBg;
|
|
row.BorderBrush = isActive ? accentBrush : borderColor;
|
|
};
|
|
row.MouseLeave += (_, _) =>
|
|
{
|
|
row.Background = isActive ? hintBg : Brushes.Transparent;
|
|
row.BorderBrush = isActive ? accentBrush : borderColor;
|
|
};
|
|
row.MouseLeftButtonUp += (_, _) =>
|
|
{
|
|
if (_isInlineSettingsSyncing)
|
|
return;
|
|
_settings.Settings.Llm.Model = capturedId;
|
|
ScheduleSettingsSave();
|
|
UpdateModelLabel();
|
|
RefreshInlineSettingsPanel();
|
|
SetStatus($"모델 전환: {capturedLabel}", spinning: false);
|
|
};
|
|
InlineModelListPanel.Children.Add(row);
|
|
}
|
|
}
|
|
|
|
private void BtnModelSelector_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
RefreshInlineSettingsPanel();
|
|
InlineSettingsPanel.IsOpen = !InlineSettingsPanel.IsOpen;
|
|
|
|
if (InlineSettingsPanel.IsOpen)
|
|
{
|
|
Dispatcher.BeginInvoke(() =>
|
|
{
|
|
if (CmbInlineModel.Items.Count > 0)
|
|
CmbInlineModel.Focus();
|
|
else
|
|
InputBox.Focus();
|
|
}, DispatcherPriority.Input);
|
|
}
|
|
}
|
|
|
|
private void OpenAgentSettingsWindow()
|
|
{
|
|
RefreshOverlaySettingsPanel();
|
|
|
|
// Scale + fade-in animation
|
|
AgentSettingsOverlay.RenderTransform = new ScaleTransform(0.98, 0.98);
|
|
AgentSettingsOverlay.RenderTransformOrigin = new Point(0.5, 0.5);
|
|
AgentSettingsOverlay.Opacity = 0;
|
|
AgentSettingsOverlay.Visibility = Visibility.Visible;
|
|
|
|
var ease = new System.Windows.Media.Animation.CubicEase();
|
|
var scaleXAnim = new System.Windows.Media.Animation.DoubleAnimation(0.98, 1.0, TimeSpan.FromMilliseconds(200)) { EasingFunction = ease };
|
|
var scaleYAnim = new System.Windows.Media.Animation.DoubleAnimation(0.98, 1.0, TimeSpan.FromMilliseconds(200)) { EasingFunction = ease };
|
|
var fadeAnim = new System.Windows.Media.Animation.DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(180));
|
|
((ScaleTransform)AgentSettingsOverlay.RenderTransform).BeginAnimation(ScaleTransform.ScaleXProperty, scaleXAnim);
|
|
((ScaleTransform)AgentSettingsOverlay.RenderTransform).BeginAnimation(ScaleTransform.ScaleYProperty, scaleYAnim);
|
|
AgentSettingsOverlay.BeginAnimation(UIElement.OpacityProperty, fadeAnim);
|
|
|
|
InlineSettingsPanel.IsOpen = false;
|
|
|
|
// 탭 RadioButton을 "공통"으로 확실히 리셋 — 재진입 시 탭/패널 불일치 방지
|
|
if (OverlayNavBasic != null)
|
|
OverlayNavBasic.IsChecked = true;
|
|
|
|
SetOverlaySection("basic");
|
|
Dispatcher.BeginInvoke(() =>
|
|
{
|
|
if (OverlayNavBasic != null)
|
|
OverlayNavBasic.Focus();
|
|
else
|
|
InputBox.Focus();
|
|
}, DispatcherPriority.Input);
|
|
}
|
|
|
|
public void OpenAgentSettingsFromExternal()
|
|
{
|
|
Dispatcher.BeginInvoke(() =>
|
|
{
|
|
Show();
|
|
Activate();
|
|
OpenAgentSettingsWindow();
|
|
}, DispatcherPriority.Input);
|
|
}
|
|
|
|
public void RefreshFromSavedSettings()
|
|
{
|
|
Dispatcher.BeginInvoke(() =>
|
|
{
|
|
ApplyAgentThemeResources();
|
|
LoadConversationSettings();
|
|
UpdatePermissionUI();
|
|
UpdateDataUsageUI();
|
|
UpdateModelLabel();
|
|
RefreshInlineSettingsPanel();
|
|
RefreshOverlaySettingsPanel();
|
|
RefreshContextUsageVisual();
|
|
UpdateTabUI();
|
|
BuildBottomBar();
|
|
RefreshDraftQueueUi();
|
|
RefreshConversationList();
|
|
if (_isStreaming && _settings.Settings.Llm.EnableChatRainbowGlow)
|
|
PlayRainbowGlow();
|
|
else
|
|
StopRainbowGlow();
|
|
}, DispatcherPriority.Input);
|
|
}
|
|
|
|
private void BtnOverlaySettingsClose_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
ApplyOverlaySettingsChanges(showToast: false, closeOverlay: true);
|
|
}
|
|
|
|
private void ApplyOverlaySettingsChanges(bool showToast, bool closeOverlay)
|
|
{
|
|
// InitializeComponent() 도중 ComboBox IsSelected="True"가 SelectionChanged를 발동시킬 수 있음
|
|
// 이 시점에 _settings가 아직 null이므로 무시
|
|
if (_settings == null) return;
|
|
var llm = _settings.Settings.Llm;
|
|
|
|
_settings.Settings.AiEnabled = true;
|
|
llm.EnableProactiveContextCompact = ChkOverlayEnableProactiveCompact?.IsChecked == true;
|
|
llm.EnableSkillSystem = ChkOverlayEnableSkillSystem?.IsChecked == true;
|
|
llm.EnableToolHooks = ChkOverlayEnableToolHooks?.IsChecked == true;
|
|
llm.EnableHookInputMutation = ChkOverlayEnableHookInputMutation?.IsChecked == true;
|
|
llm.EnableHookPermissionUpdate = ChkOverlayEnableHookPermissionUpdate?.IsChecked == true;
|
|
llm.EnableCoworkVerification = ChkOverlayEnableCoworkVerification?.IsChecked == true;
|
|
llm.CoworkOnComplete = (CmbOverlayCoworkOnComplete?.SelectedItem as System.Windows.Controls.ComboBoxItem)?.Tag?.ToString() ?? "none";
|
|
llm.AutoPreview = (CmbOverlayAutoPreview?.SelectedItem as System.Windows.Controls.ComboBoxItem)?.Tag?.ToString() ?? "off";
|
|
llm.Code.EnableCodeVerification = ChkOverlayEnableCodeVerification?.IsChecked == true;
|
|
llm.Code.EnableCodeReview = ChkOverlayEnableCodeReview?.IsChecked == true;
|
|
llm.EnableImageInput = ChkOverlayEnableImageInput?.IsChecked == true;
|
|
llm.EnableParallelTools = ChkOverlayEnableParallelTools?.IsChecked == true;
|
|
llm.EnableProjectRules = ChkOverlayEnableProjectRules?.IsChecked == true;
|
|
llm.EnableAgentMemory = ChkOverlayEnableAgentMemory?.IsChecked == true;
|
|
llm.EnableIbmDiagnosticLog = ChkOverlayEnableIbmDiagnosticLog?.IsChecked == true;
|
|
llm.Code.EnableWorktreeTools = ChkOverlayEnableWorktreeTools?.IsChecked == true;
|
|
llm.Code.EnableTeamTools = ChkOverlayEnableTeamTools?.IsChecked == true;
|
|
llm.Code.EnableCronTools = ChkOverlayEnableCronTools?.IsChecked == true;
|
|
llm.Code.MascotLevel = (CmbOverlayMascotLevel?.SelectedItem as System.Windows.Controls.ComboBoxItem)?.Tag?.ToString() ?? "none";
|
|
llm.WorkflowVisualizer = ChkOverlayWorkflowVisualizer?.IsChecked == true;
|
|
llm.ShowTotalCallStats = ChkOverlayShowTotalCallStats?.IsChecked == true;
|
|
llm.EnableAuditLog = ChkOverlayEnableAuditLog?.IsChecked == true;
|
|
llm.EnableChatRainbowGlow = ChkOverlayEnableChatRainbowGlow?.IsChecked == true;
|
|
_settings.Settings.Launcher.EnableChatIconRandomAnimation = ChkOverlayEnableChatIconRandomAnim?.IsChecked == true;
|
|
_settings.Settings.Launcher.ChatIconGlowIntensity =
|
|
(CmbOverlayChatIconGlow?.SelectedItem as System.Windows.Controls.ComboBoxItem)?.Tag?.ToString() ?? "medium";
|
|
// V2 뷰어/렌더링 전환 완료 — 항상 V2 사용
|
|
|
|
CommitOverlayEndpointInput(normalizeOnInvalid: true);
|
|
CommitOverlayApiKeyInput();
|
|
CommitOverlayModelInput(normalizeOnInvalid: true);
|
|
CommitOverlayNumericInput(TxtOverlayContextCompactTriggerPercent, llm.ContextCompactTriggerPercent, 10, 95, value => llm.ContextCompactTriggerPercent = value, normalizeOnInvalid: true);
|
|
CommitOverlayNumericInput(TxtOverlayMaxContextTokens, llm.MaxContextTokens, 1024, 1_000_000, value => llm.MaxContextTokens = value, normalizeOnInvalid: true);
|
|
CommitOverlayTemperatureInput(normalizeOnInvalid: true);
|
|
CommitOverlayNumericInput(TxtOverlayMaxRetryOnError, llm.MaxRetryOnError, 0, 10, value => llm.MaxRetryOnError = value, normalizeOnInvalid: true);
|
|
CommitOverlayNumericInput(TxtOverlayMaxAgentIterations, llm.MaxAgentIterations, 1, 200, value => llm.MaxAgentIterations = value, normalizeOnInvalid: true);
|
|
CommitOverlayNumericInput(TxtOverlayFreeTierDelaySeconds, llm.FreeTierDelaySeconds, 0, 60, value => llm.FreeTierDelaySeconds = value, normalizeOnInvalid: true);
|
|
CommitOverlayNumericInput(TxtOverlayMaxSubAgents, llm.MaxSubAgents, 1, 10, value => llm.MaxSubAgents = value, normalizeOnInvalid: true);
|
|
CommitOverlayNumericInput(TxtOverlayToolHookTimeoutMs, llm.ToolHookTimeoutMs, 3000, 30000, value => llm.ToolHookTimeoutMs = value, normalizeOnInvalid: true);
|
|
CommitOverlayNumericInput(TxtOverlayMaxFavoriteSlashCommands, llm.MaxFavoriteSlashCommands, 1, 30, value => llm.MaxFavoriteSlashCommands = value, normalizeOnInvalid: true);
|
|
CommitOverlayNumericInput(TxtOverlayMaxRecentSlashCommands, llm.MaxRecentSlashCommands, 5, 50, value => llm.MaxRecentSlashCommands = value, normalizeOnInvalid: true);
|
|
CommitOverlayNumericInput(TxtOverlayPlanDiffMediumCount, llm.PlanDiffSeverityMediumCount, 1, 999, value => llm.PlanDiffSeverityMediumCount = value, normalizeOnInvalid: true);
|
|
CommitOverlayNumericInput(TxtOverlayPlanDiffHighCount, llm.PlanDiffSeverityHighCount, 1, 999, value => llm.PlanDiffSeverityHighCount = value, normalizeOnInvalid: true);
|
|
CommitOverlayNumericInput(TxtOverlayPlanDiffMediumRatio, llm.PlanDiffSeverityMediumRatioPercent, 1, 100, value => llm.PlanDiffSeverityMediumRatioPercent = value, normalizeOnInvalid: true);
|
|
CommitOverlayNumericInput(TxtOverlayPlanDiffHighRatio, llm.PlanDiffSeverityHighRatioPercent, 1, 100, value => llm.PlanDiffSeverityHighRatioPercent = value, normalizeOnInvalid: true);
|
|
if (TxtOverlayPdfExportPath != null)
|
|
llm.PdfExportPath = TxtOverlayPdfExportPath.Text.Trim();
|
|
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
if (closeOverlay)
|
|
{
|
|
var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(120));
|
|
fadeOut.Completed += (_, _) =>
|
|
{
|
|
AgentSettingsOverlay.Visibility = Visibility.Collapsed;
|
|
AgentSettingsOverlay.BeginAnimation(UIElement.OpacityProperty, null);
|
|
AgentSettingsOverlay.Opacity = 1;
|
|
};
|
|
AgentSettingsOverlay.BeginAnimation(UIElement.OpacityProperty, fadeOut);
|
|
}
|
|
if (showToast)
|
|
ShowToast("AX Agent 설정이 저장되었습니다.");
|
|
InputBox.Focus();
|
|
}
|
|
|
|
private void PersistOverlaySettingsState(bool refreshOverlayDeferredInputs)
|
|
{
|
|
ScheduleSettingsSave();
|
|
_appState.LoadFromSettings(_settings);
|
|
ApplyAgentThemeResources();
|
|
UpdatePermissionUI();
|
|
UpdateDataUsageUI();
|
|
SyncWorkflowVisualizerWindow();
|
|
SaveConversationSettings();
|
|
RefreshInlineSettingsPanel();
|
|
UpdateModelLabel();
|
|
UpdateTabUI();
|
|
RefreshOverlayVisualState(refreshOverlayDeferredInputs);
|
|
}
|
|
|
|
private void RefreshOverlayVisualState(bool loadDeferredInputs)
|
|
{
|
|
var llm = _settings.Settings.Llm;
|
|
var service = NormalizeOverlayService(llm.Service);
|
|
var models = GetModelCandidates(service);
|
|
|
|
_isOverlaySettingsSyncing = true;
|
|
try
|
|
{
|
|
if (CmbOverlayService != null)
|
|
{
|
|
CmbOverlayService.Items.Clear();
|
|
foreach (var svc in new[] { "ollama", "vllm", "gemini", "claude" })
|
|
{
|
|
CmbOverlayService.Items.Add(new ComboBoxItem
|
|
{
|
|
Content = ServiceLabel(svc),
|
|
Tag = svc
|
|
});
|
|
}
|
|
|
|
CmbOverlayService.SelectedItem = CmbOverlayService.Items
|
|
.OfType<ComboBoxItem>()
|
|
.FirstOrDefault(i => string.Equals(i.Tag as string, service, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
if (CmbOverlayModel != null)
|
|
{
|
|
CmbOverlayModel.Items.Clear();
|
|
foreach (var model in models)
|
|
{
|
|
CmbOverlayModel.Items.Add(new ComboBoxItem
|
|
{
|
|
Content = model.Label,
|
|
Tag = model.Id
|
|
});
|
|
}
|
|
|
|
CmbOverlayModel.SelectedItem = CmbOverlayModel.Items
|
|
.OfType<ComboBoxItem>()
|
|
.FirstOrDefault(i => string.Equals(i.Tag as string, llm.Model, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (CmbOverlayModel.SelectedItem == null && CmbOverlayModel.Items.Count > 0)
|
|
CmbOverlayModel.SelectedIndex = 0;
|
|
}
|
|
|
|
var serviceText = ServiceLabel(service);
|
|
var modelText = string.IsNullOrWhiteSpace(llm.Model)
|
|
? "미선택"
|
|
: (models.FirstOrDefault(m => string.Equals(m.Id, llm.Model, StringComparison.OrdinalIgnoreCase)).Label ?? llm.Model);
|
|
|
|
ViewModel.ServiceLabel = serviceText;
|
|
ViewModel.ModelLabel = modelText;
|
|
|
|
PopulateOverlayMoodCombo();
|
|
|
|
if (loadDeferredInputs)
|
|
{
|
|
if (ChkOverlayAiEnabled != null)
|
|
ChkOverlayAiEnabled.IsChecked = true;
|
|
if (TxtOverlayServiceEndpoint != null)
|
|
TxtOverlayServiceEndpoint.Text = GetOverlayServiceEndpoint(service);
|
|
if (TxtOverlayServiceApiKey != null)
|
|
TxtOverlayServiceApiKey.Text = GetOverlayServiceApiKey(service);
|
|
if (TxtOverlayContextCompactTriggerPercent != null)
|
|
TxtOverlayContextCompactTriggerPercent.Text = Math.Clamp(llm.ContextCompactTriggerPercent, 10, 95).ToString();
|
|
if (TxtOverlayMaxContextTokens != null)
|
|
TxtOverlayMaxContextTokens.Text = Math.Clamp(llm.MaxContextTokens, 1024, 1_000_000).ToString();
|
|
if (TxtOverlayTemperature != null)
|
|
TxtOverlayTemperature.Text = Math.Round(Math.Clamp(llm.Temperature, 0.0, 2.0), 1).ToString("0.0");
|
|
if (SldOverlayTemperature != null)
|
|
SldOverlayTemperature.Value = Math.Round(Math.Clamp(llm.Temperature, 0.0, 2.0), 1);
|
|
if (TxtOverlayTemperatureValue != null)
|
|
TxtOverlayTemperatureValue.Text = Math.Round(Math.Clamp(llm.Temperature, 0.0, 2.0), 1).ToString("0.0");
|
|
RefreshOverlayTemperatureModeButtons();
|
|
if (TxtOverlayMaxRetryOnError != null)
|
|
TxtOverlayMaxRetryOnError.Text = Math.Clamp(llm.MaxRetryOnError, 0, 10).ToString();
|
|
if (SldOverlayMaxRetryOnError != null)
|
|
SldOverlayMaxRetryOnError.Value = Math.Clamp(llm.MaxRetryOnError, 0, 10);
|
|
if (TxtOverlayMaxRetryOnErrorValue != null)
|
|
TxtOverlayMaxRetryOnErrorValue.Text = Math.Clamp(llm.MaxRetryOnError, 0, 10).ToString();
|
|
if (TxtOverlayMaxAgentIterations != null)
|
|
TxtOverlayMaxAgentIterations.Text = Math.Clamp(llm.MaxAgentIterations, 1, 200).ToString();
|
|
if (SldOverlayMaxAgentIterations != null)
|
|
SldOverlayMaxAgentIterations.Value = Math.Clamp(llm.MaxAgentIterations, 1, 100);
|
|
if (TxtOverlayMaxAgentIterationsValue != null)
|
|
TxtOverlayMaxAgentIterationsValue.Text = Math.Clamp(llm.MaxAgentIterations, 1, 100).ToString();
|
|
if (TxtOverlayFreeTierDelaySeconds != null)
|
|
TxtOverlayFreeTierDelaySeconds.Text = Math.Clamp(llm.FreeTierDelaySeconds, 0, 60).ToString();
|
|
if (SldOverlayFreeTierDelaySeconds != null)
|
|
SldOverlayFreeTierDelaySeconds.Value = Math.Clamp(llm.FreeTierDelaySeconds, 0, 60);
|
|
if (TxtOverlayFreeTierDelaySecondsValue != null)
|
|
TxtOverlayFreeTierDelaySecondsValue.Text = Math.Clamp(llm.FreeTierDelaySeconds, 0, 60).ToString();
|
|
if (TxtOverlayMaxSubAgents != null)
|
|
TxtOverlayMaxSubAgents.Text = Math.Clamp(llm.MaxSubAgents, 1, 10).ToString();
|
|
if (SldOverlayMaxSubAgents != null)
|
|
SldOverlayMaxSubAgents.Value = Math.Clamp(llm.MaxSubAgents, 1, 10);
|
|
if (TxtOverlayMaxSubAgentsValue != null)
|
|
TxtOverlayMaxSubAgentsValue.Text = Math.Clamp(llm.MaxSubAgents, 1, 10).ToString();
|
|
if (TxtOverlayToolHookTimeoutMs != null)
|
|
TxtOverlayToolHookTimeoutMs.Text = Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000).ToString();
|
|
if (SldOverlayToolHookTimeoutMs != null)
|
|
SldOverlayToolHookTimeoutMs.Value = Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000);
|
|
if (TxtOverlayToolHookTimeoutMsValue != null)
|
|
TxtOverlayToolHookTimeoutMsValue.Text = $"{Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000) / 1000}s";
|
|
if (TxtOverlayMaxFavoriteSlashCommands != null)
|
|
TxtOverlayMaxFavoriteSlashCommands.Text = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30).ToString();
|
|
if (SldOverlayMaxFavoriteSlashCommands != null)
|
|
SldOverlayMaxFavoriteSlashCommands.Value = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30);
|
|
if (TxtOverlayMaxFavoriteSlashCommandsValue != null)
|
|
TxtOverlayMaxFavoriteSlashCommandsValue.Text = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30).ToString();
|
|
if (TxtOverlayMaxRecentSlashCommands != null)
|
|
TxtOverlayMaxRecentSlashCommands.Text = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50).ToString();
|
|
if (SldOverlayMaxRecentSlashCommands != null)
|
|
SldOverlayMaxRecentSlashCommands.Value = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50);
|
|
if (TxtOverlayMaxRecentSlashCommandsValue != null)
|
|
TxtOverlayMaxRecentSlashCommandsValue.Text = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50).ToString();
|
|
if (TxtOverlayPlanDiffMediumCount != null)
|
|
TxtOverlayPlanDiffMediumCount.Text = Math.Clamp(llm.PlanDiffSeverityMediumCount, 1, 999).ToString();
|
|
if (TxtOverlayPlanDiffHighCount != null)
|
|
TxtOverlayPlanDiffHighCount.Text = Math.Clamp(llm.PlanDiffSeverityHighCount, 1, 999).ToString();
|
|
if (TxtOverlayPlanDiffMediumRatio != null)
|
|
TxtOverlayPlanDiffMediumRatio.Text = Math.Clamp(llm.PlanDiffSeverityMediumRatioPercent, 1, 100).ToString();
|
|
if (TxtOverlayPlanDiffHighRatio != null)
|
|
TxtOverlayPlanDiffHighRatio.Text = Math.Clamp(llm.PlanDiffSeverityHighRatioPercent, 1, 100).ToString();
|
|
if (TxtOverlayPdfExportPath != null)
|
|
TxtOverlayPdfExportPath.Text = llm.PdfExportPath ?? "";
|
|
if (ChkOverlayEnableProactiveCompact != null)
|
|
ChkOverlayEnableProactiveCompact.IsChecked = llm.EnableProactiveContextCompact;
|
|
if (ChkOverlayEnableSkillSystem != null)
|
|
ChkOverlayEnableSkillSystem.IsChecked = llm.EnableSkillSystem;
|
|
if (ChkOverlayEnableToolHooks != null)
|
|
ChkOverlayEnableToolHooks.IsChecked = llm.EnableToolHooks;
|
|
if (ChkOverlayEnableHookInputMutation != null)
|
|
ChkOverlayEnableHookInputMutation.IsChecked = llm.EnableHookInputMutation;
|
|
if (ChkOverlayEnableHookPermissionUpdate != null)
|
|
ChkOverlayEnableHookPermissionUpdate.IsChecked = llm.EnableHookPermissionUpdate;
|
|
if (ChkOverlayEnableCoworkVerification != null)
|
|
ChkOverlayEnableCoworkVerification.IsChecked = llm.EnableCoworkVerification;
|
|
SelectComboBoxByTag(CmbOverlayCoworkOnComplete, llm.CoworkOnComplete);
|
|
SelectComboBoxByTag(CmbOverlayAutoPreview, llm.AutoPreview);
|
|
if (ChkOverlayEnableCodeVerification != null)
|
|
ChkOverlayEnableCodeVerification.IsChecked = llm.Code.EnableCodeVerification;
|
|
if (ChkOverlayEnableCodeReview != null)
|
|
ChkOverlayEnableCodeReview.IsChecked = llm.Code.EnableCodeReview;
|
|
if (ChkOverlayEnableImageInput != null)
|
|
ChkOverlayEnableImageInput.IsChecked = llm.EnableImageInput;
|
|
if (ChkOverlayEnableParallelTools != null)
|
|
ChkOverlayEnableParallelTools.IsChecked = llm.EnableParallelTools;
|
|
if (ChkOverlayEnableProjectRules != null)
|
|
ChkOverlayEnableProjectRules.IsChecked = llm.EnableProjectRules;
|
|
if (ChkOverlayEnableAgentMemory != null)
|
|
ChkOverlayEnableAgentMemory.IsChecked = llm.EnableAgentMemory;
|
|
if (ChkOverlayEnableIbmDiagnosticLog != null)
|
|
ChkOverlayEnableIbmDiagnosticLog.IsChecked = llm.EnableIbmDiagnosticLog;
|
|
if (ChkOverlayEnableWorktreeTools != null)
|
|
ChkOverlayEnableWorktreeTools.IsChecked = llm.Code.EnableWorktreeTools;
|
|
if (ChkOverlayEnableTeamTools != null)
|
|
ChkOverlayEnableTeamTools.IsChecked = llm.Code.EnableTeamTools;
|
|
if (ChkOverlayEnableCronTools != null)
|
|
ChkOverlayEnableCronTools.IsChecked = llm.Code.EnableCronTools;
|
|
if (CmbOverlayMascotLevel != null)
|
|
SelectComboBoxByTag(CmbOverlayMascotLevel, llm.Code.MascotLevel ?? "none");
|
|
if (ChkOverlayWorkflowVisualizer != null)
|
|
ChkOverlayWorkflowVisualizer.IsChecked = llm.WorkflowVisualizer;
|
|
if (ChkOverlayShowTotalCallStats != null)
|
|
ChkOverlayShowTotalCallStats.IsChecked = llm.ShowTotalCallStats;
|
|
if (ChkOverlayEnableAuditLog != null)
|
|
ChkOverlayEnableAuditLog.IsChecked = llm.EnableAuditLog;
|
|
if (ChkOverlayEnableDetailedLog != null)
|
|
ChkOverlayEnableDetailedLog.IsChecked = llm.EnableDetailedLog;
|
|
if (ChkOverlayEnableRawLlmLog != null)
|
|
ChkOverlayEnableRawLlmLog.IsChecked = llm.EnableRawLlmLog;
|
|
if (ChkOverlayEnableChatRainbowGlow != null)
|
|
ChkOverlayEnableChatRainbowGlow.IsChecked = llm.EnableChatRainbowGlow;
|
|
if (ChkOverlayEnableChatIconRandomAnim != null)
|
|
ChkOverlayEnableChatIconRandomAnim.IsChecked = _settings.Settings.Launcher.EnableChatIconRandomAnimation;
|
|
if (CmbOverlayChatIconGlow != null)
|
|
SelectComboBoxByTag(CmbOverlayChatIconGlow, _settings.Settings.Launcher.ChatIconGlowIntensity ?? "medium");
|
|
// V2 뷰어/렌더링 전환 완료 — 토글 제거됨
|
|
}
|
|
|
|
RefreshOverlayThemeCards();
|
|
RefreshOverlayServiceCards();
|
|
RefreshOverlayModeButtons();
|
|
RefreshOverlayTokenPresetCards();
|
|
RefreshOverlayServiceFieldLabels(service);
|
|
RefreshOverlayServiceFieldVisibility(service);
|
|
BuildOverlayRegisteredModelsPanel(service);
|
|
RefreshOverlayAdvancedChoiceButtons();
|
|
RefreshOverlayRetentionButtons();
|
|
RefreshOverlayStorageSummary();
|
|
}
|
|
finally
|
|
{
|
|
_isOverlaySettingsSyncing = false;
|
|
}
|
|
}
|
|
|
|
private static string NormalizeOverlayService(string? service)
|
|
=> string.Equals(service, "sigmoid", StringComparison.OrdinalIgnoreCase) ? "claude" : (service ?? "ollama").Trim().ToLowerInvariant();
|
|
|
|
private void CommitOverlayEndpointInput(bool normalizeOnInvalid)
|
|
{
|
|
var service = NormalizeOverlayService(_settings.Settings.Llm.Service);
|
|
var endpoint = TxtOverlayServiceEndpoint?.Text.Trim() ?? "";
|
|
ClearOverlayValidation(TxtOverlayServiceEndpoint);
|
|
switch (service)
|
|
{
|
|
case "ollama":
|
|
_settings.Settings.Llm.OllamaEndpoint = endpoint;
|
|
_settings.Settings.Llm.Endpoint = endpoint;
|
|
break;
|
|
case "vllm":
|
|
_settings.Settings.Llm.VllmEndpoint = endpoint;
|
|
_settings.Settings.Llm.Endpoint = endpoint;
|
|
break;
|
|
default:
|
|
_settings.Settings.Llm.Endpoint = endpoint;
|
|
break;
|
|
}
|
|
|
|
if (normalizeOnInvalid && TxtOverlayServiceEndpoint != null)
|
|
TxtOverlayServiceEndpoint.Text = endpoint;
|
|
}
|
|
|
|
private void CommitOverlayApiKeyInput()
|
|
{
|
|
var service = NormalizeOverlayService(_settings.Settings.Llm.Service);
|
|
var apiKey = TxtOverlayServiceApiKey?.Text ?? "";
|
|
switch (service)
|
|
{
|
|
case "ollama":
|
|
_settings.Settings.Llm.OllamaApiKey = apiKey;
|
|
_settings.Settings.Llm.ApiKey = apiKey;
|
|
break;
|
|
case "vllm":
|
|
_settings.Settings.Llm.VllmApiKey = apiKey;
|
|
_settings.Settings.Llm.ApiKey = apiKey;
|
|
break;
|
|
case "gemini":
|
|
_settings.Settings.Llm.GeminiApiKey = apiKey;
|
|
_settings.Settings.Llm.ApiKey = apiKey;
|
|
break;
|
|
default:
|
|
_settings.Settings.Llm.ClaudeApiKey = apiKey;
|
|
_settings.Settings.Llm.ApiKey = apiKey;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void CommitOverlayModelInput(bool normalizeOnInvalid)
|
|
{
|
|
if (TxtOverlayModelInput == null || TxtOverlayModelInput.Visibility != Visibility.Visible)
|
|
return;
|
|
|
|
var value = TxtOverlayModelInput?.Text.Trim() ?? "";
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
MarkOverlayValidation(TxtOverlayModelInput, "모델명을 입력하세요.");
|
|
if (normalizeOnInvalid && TxtOverlayModelInput != null)
|
|
{
|
|
TxtOverlayModelInput.Text = _settings.Settings.Llm.Model ?? "";
|
|
ClearOverlayValidation(TxtOverlayModelInput);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
ClearOverlayValidation(TxtOverlayModelInput);
|
|
_settings.Settings.Llm.Model = value;
|
|
var service = NormalizeOverlayService(_settings.Settings.Llm.Service);
|
|
switch (service)
|
|
{
|
|
case "ollama":
|
|
_settings.Settings.Llm.OllamaModel = value;
|
|
break;
|
|
case "vllm":
|
|
_settings.Settings.Llm.VllmModel = value;
|
|
break;
|
|
case "gemini":
|
|
_settings.Settings.Llm.GeminiModel = value;
|
|
break;
|
|
default:
|
|
_settings.Settings.Llm.ClaudeModel = value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private bool CommitOverlayNumericInput(TextBox? textBox, int currentValue, int min, int max, Action<int> applyValue, bool normalizeOnInvalid)
|
|
{
|
|
if (textBox == null)
|
|
return false;
|
|
|
|
if (!int.TryParse(textBox.Text?.Trim(), out var parsed))
|
|
{
|
|
MarkOverlayValidation(textBox, $"{min}~{max} 사이 숫자를 입력하세요.");
|
|
if (normalizeOnInvalid)
|
|
{
|
|
textBox.Text = Math.Clamp(currentValue, min, max).ToString();
|
|
ClearOverlayValidation(textBox);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
parsed = Math.Clamp(parsed, min, max);
|
|
applyValue(parsed);
|
|
textBox.Text = parsed.ToString();
|
|
ClearOverlayValidation(textBox);
|
|
return true;
|
|
}
|
|
|
|
private LlmSettings? TryGetOverlayLlmSettings()
|
|
=> _settings?.Settings?.Llm;
|
|
|
|
private void MarkOverlayValidation(Control? control, string message)
|
|
{
|
|
if (control == null)
|
|
return;
|
|
|
|
control.BorderBrush = BrushFromHex("#DC2626");
|
|
control.ToolTip = message;
|
|
}
|
|
|
|
private void ClearOverlayValidation(Control? control)
|
|
{
|
|
if (control == null)
|
|
return;
|
|
|
|
control.BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
|
control.ClearValue(ToolTipProperty);
|
|
}
|
|
|
|
private void CommitOverlayModelSelection(string modelId)
|
|
{
|
|
_settings.Settings.Llm.Model = modelId;
|
|
var service = NormalizeOverlayService(_settings.Settings.Llm.Service);
|
|
switch (service)
|
|
{
|
|
case "ollama":
|
|
_settings.Settings.Llm.OllamaModel = modelId;
|
|
break;
|
|
case "vllm":
|
|
_settings.Settings.Llm.VllmModel = modelId;
|
|
break;
|
|
case "gemini":
|
|
_settings.Settings.Llm.GeminiModel = modelId;
|
|
break;
|
|
default:
|
|
_settings.Settings.Llm.ClaudeModel = modelId;
|
|
break;
|
|
}
|
|
|
|
if (TxtOverlayModelInput != null && TxtOverlayModelInput.Visibility == Visibility.Visible)
|
|
{
|
|
TxtOverlayModelInput.Text = modelId;
|
|
ClearOverlayValidation(TxtOverlayModelInput);
|
|
}
|
|
}
|
|
|
|
private void ChkOverlayAiEnabled_Changed(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
_settings.Settings.AiEnabled = true;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void ChkOverlayVllmAllowInsecureTls_Changed(object sender, RoutedEventArgs e)
|
|
{
|
|
// vLLM SSL 우회는 모델 등록 단계에서만 관리합니다.
|
|
}
|
|
|
|
private void TxtOverlayServiceEndpoint_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
CommitOverlayEndpointInput(normalizeOnInvalid: false);
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void TxtOverlayServiceApiKey_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
CommitOverlayApiKeyInput();
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void TxtOverlayModelInput_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
CommitOverlayModelInput(normalizeOnInvalid: false);
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void RefreshOverlayAdvancedChoiceButtons()
|
|
{
|
|
// ToggleSwitch 기반으로 바뀌면서 별도 버튼 시각 동기화는 사용하지 않습니다.
|
|
}
|
|
|
|
private void TxtOverlayContextCompactTriggerPercent_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
if (CommitOverlayNumericInput(TxtOverlayContextCompactTriggerPercent, _settings.Settings.Llm.ContextCompactTriggerPercent, 10, 95, value => _settings.Settings.Llm.ContextCompactTriggerPercent = value, normalizeOnInvalid: false))
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void TxtOverlayMaxContextTokens_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
if (CommitOverlayNumericInput(TxtOverlayMaxContextTokens, _settings.Settings.Llm.MaxContextTokens, 1024, 1_000_000, value => _settings.Settings.Llm.MaxContextTokens = value, normalizeOnInvalid: false))
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private bool CommitOverlayTemperatureInput(bool normalizeOnInvalid)
|
|
{
|
|
if (TxtOverlayTemperature == null)
|
|
return false;
|
|
|
|
if (_settings.Settings.Llm.UseAutomaticProfileTemperature)
|
|
return false;
|
|
|
|
var raw = TxtOverlayTemperature.Text.Trim();
|
|
if (!double.TryParse(raw, out var parsed))
|
|
{
|
|
ClearOverlayValidation(TxtOverlayTemperature);
|
|
if (normalizeOnInvalid)
|
|
TxtOverlayTemperature.Text = Math.Round(Math.Clamp(_settings.Settings.Llm.Temperature, 0.0, 2.0), 1).ToString("0.0");
|
|
return false;
|
|
}
|
|
|
|
var normalized = Math.Round(Math.Clamp(parsed, 0.0, 2.0), 1);
|
|
var changed = Math.Abs(_settings.Settings.Llm.Temperature - normalized) > 0.0001;
|
|
_settings.Settings.Llm.Temperature = normalized;
|
|
ClearOverlayValidation(TxtOverlayTemperature);
|
|
if (normalizeOnInvalid || changed)
|
|
TxtOverlayTemperature.Text = normalized.ToString("0.0");
|
|
return changed;
|
|
}
|
|
|
|
private void TxtOverlayTemperature_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
if (CommitOverlayTemperatureInput(normalizeOnInvalid: false))
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void SldOverlayTemperature_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
|
{
|
|
var llm = TryGetOverlayLlmSettings();
|
|
if (_isOverlaySettingsSyncing || llm == null)
|
|
return;
|
|
|
|
if (llm.UseAutomaticProfileTemperature)
|
|
return;
|
|
|
|
var value = Math.Round(Math.Clamp(e.NewValue, 0.0, 2.0), 1);
|
|
llm.Temperature = value;
|
|
if (TxtOverlayTemperature != null)
|
|
TxtOverlayTemperature.Text = value.ToString("0.0");
|
|
if (TxtOverlayTemperatureValue != null)
|
|
TxtOverlayTemperatureValue.Text = value.ToString("0.0");
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void OverlayTemperatureAutoCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
_settings.Settings.Llm.UseAutomaticProfileTemperature = true;
|
|
RefreshOverlayTemperatureModeButtons();
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void OverlayTemperatureCustomCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
_settings.Settings.Llm.UseAutomaticProfileTemperature = false;
|
|
RefreshOverlayTemperatureModeButtons();
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void SldOverlayMaxRetryOnError_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
|
{
|
|
var llm = TryGetOverlayLlmSettings();
|
|
if (_isOverlaySettingsSyncing || llm == null)
|
|
return;
|
|
|
|
var value = (int)Math.Round(Math.Clamp(e.NewValue, 0, 10));
|
|
llm.MaxRetryOnError = value;
|
|
if (TxtOverlayMaxRetryOnError != null)
|
|
TxtOverlayMaxRetryOnError.Text = value.ToString();
|
|
if (TxtOverlayMaxRetryOnErrorValue != null)
|
|
TxtOverlayMaxRetryOnErrorValue.Text = value.ToString();
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void SldOverlayMaxAgentIterations_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
|
{
|
|
var llm = TryGetOverlayLlmSettings();
|
|
if (_isOverlaySettingsSyncing || llm == null)
|
|
return;
|
|
|
|
var value = (int)Math.Round(Math.Clamp(e.NewValue, 1, 100));
|
|
llm.MaxAgentIterations = value;
|
|
if (TxtOverlayMaxAgentIterations != null)
|
|
TxtOverlayMaxAgentIterations.Text = value.ToString();
|
|
if (TxtOverlayMaxAgentIterationsValue != null)
|
|
TxtOverlayMaxAgentIterationsValue.Text = value.ToString();
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void SldOverlayFreeTierDelaySeconds_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
|
{
|
|
var llm = TryGetOverlayLlmSettings();
|
|
if (_isOverlaySettingsSyncing || llm == null)
|
|
return;
|
|
|
|
var value = (int)Math.Round(Math.Clamp(e.NewValue, 0, 60));
|
|
llm.FreeTierDelaySeconds = value;
|
|
if (TxtOverlayFreeTierDelaySeconds != null)
|
|
TxtOverlayFreeTierDelaySeconds.Text = value.ToString();
|
|
if (TxtOverlayFreeTierDelaySecondsValue != null)
|
|
TxtOverlayFreeTierDelaySecondsValue.Text = value.ToString();
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void SldOverlayMaxSubAgents_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
|
{
|
|
var llm = TryGetOverlayLlmSettings();
|
|
if (_isOverlaySettingsSyncing || llm == null)
|
|
return;
|
|
|
|
var value = (int)Math.Round(Math.Clamp(e.NewValue, 1, 10));
|
|
llm.MaxSubAgents = value;
|
|
if (TxtOverlayMaxSubAgents != null)
|
|
TxtOverlayMaxSubAgents.Text = value.ToString();
|
|
if (TxtOverlayMaxSubAgentsValue != null)
|
|
TxtOverlayMaxSubAgentsValue.Text = value.ToString();
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void SldOverlayToolHookTimeoutMs_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
|
{
|
|
var llm = TryGetOverlayLlmSettings();
|
|
if (_isOverlaySettingsSyncing || llm == null)
|
|
return;
|
|
|
|
var value = (int)Math.Round(Math.Clamp(e.NewValue, 3000, 30000));
|
|
llm.ToolHookTimeoutMs = value;
|
|
if (TxtOverlayToolHookTimeoutMs != null)
|
|
TxtOverlayToolHookTimeoutMs.Text = value.ToString();
|
|
if (TxtOverlayToolHookTimeoutMsValue != null)
|
|
TxtOverlayToolHookTimeoutMsValue.Text = $"{value / 1000}s";
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void SldOverlaySlashPopupPageSize_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
|
{
|
|
var llm = TryGetOverlayLlmSettings();
|
|
if (_isOverlaySettingsSyncing || llm == null)
|
|
return;
|
|
|
|
var value = (int)Math.Round(Math.Clamp(e.NewValue, 3, 20));
|
|
llm.SlashPopupPageSize = value;
|
|
if (TxtOverlaySlashPopupPageSize != null)
|
|
TxtOverlaySlashPopupPageSize.Text = value.ToString();
|
|
if (TxtOverlaySlashPopupPageSizeValue != null)
|
|
TxtOverlaySlashPopupPageSizeValue.Text = value.ToString();
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void SldOverlayMaxFavoriteSlashCommands_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
|
{
|
|
var llm = TryGetOverlayLlmSettings();
|
|
if (_isOverlaySettingsSyncing || llm == null)
|
|
return;
|
|
|
|
var value = (int)Math.Round(Math.Clamp(e.NewValue, 1, 30));
|
|
llm.MaxFavoriteSlashCommands = value;
|
|
if (TxtOverlayMaxFavoriteSlashCommands != null)
|
|
TxtOverlayMaxFavoriteSlashCommands.Text = value.ToString();
|
|
if (TxtOverlayMaxFavoriteSlashCommandsValue != null)
|
|
TxtOverlayMaxFavoriteSlashCommandsValue.Text = value.ToString();
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void SldOverlayMaxRecentSlashCommands_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
|
{
|
|
var llm = TryGetOverlayLlmSettings();
|
|
if (_isOverlaySettingsSyncing || llm == null)
|
|
return;
|
|
|
|
var value = (int)Math.Round(Math.Clamp(e.NewValue, 5, 50));
|
|
llm.MaxRecentSlashCommands = value;
|
|
if (TxtOverlayMaxRecentSlashCommands != null)
|
|
TxtOverlayMaxRecentSlashCommands.Text = value.ToString();
|
|
if (TxtOverlayMaxRecentSlashCommandsValue != null)
|
|
TxtOverlayMaxRecentSlashCommandsValue.Text = value.ToString();
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void OverlayCompactPresetCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (sender is not Border border || border.Tag is not string tag || !int.TryParse(tag, out var value))
|
|
return;
|
|
|
|
_settings.Settings.Llm.ContextCompactTriggerPercent = Math.Clamp(value, 10, 95);
|
|
if (TxtOverlayContextCompactTriggerPercent != null)
|
|
TxtOverlayContextCompactTriggerPercent.Text = _settings.Settings.Llm.ContextCompactTriggerPercent.ToString();
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void OverlayContextPresetCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (sender is not Border border || border.Tag is not string tag || !int.TryParse(tag, out var value))
|
|
return;
|
|
|
|
_settings.Settings.Llm.MaxContextTokens = Math.Clamp(value, 1024, 1_000_000);
|
|
if (TxtOverlayMaxContextTokens != null)
|
|
TxtOverlayMaxContextTokens.Text = _settings.Settings.Llm.MaxContextTokens.ToString();
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void TxtOverlayMaxRetryOnError_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
if (CommitOverlayNumericInput(TxtOverlayMaxRetryOnError, _settings.Settings.Llm.MaxRetryOnError, 0, 10, value => _settings.Settings.Llm.MaxRetryOnError = value, normalizeOnInvalid: false))
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void TxtOverlayMaxAgentIterations_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
if (CommitOverlayNumericInput(TxtOverlayMaxAgentIterations, _settings.Settings.Llm.MaxAgentIterations, 1, 200, value => _settings.Settings.Llm.MaxAgentIterations = value, normalizeOnInvalid: false))
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void TxtOverlayFreeTierDelaySeconds_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
if (CommitOverlayNumericInput(TxtOverlayFreeTierDelaySeconds, _settings.Settings.Llm.FreeTierDelaySeconds, 0, 60, value => _settings.Settings.Llm.FreeTierDelaySeconds = value, normalizeOnInvalid: false))
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void TxtOverlayMaxSubAgents_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
if (CommitOverlayNumericInput(TxtOverlayMaxSubAgents, _settings.Settings.Llm.MaxSubAgents, 1, 10, value => _settings.Settings.Llm.MaxSubAgents = value, normalizeOnInvalid: false))
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void TxtOverlaySlashPopupPageSize_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
if (CommitOverlayNumericInput(TxtOverlaySlashPopupPageSize, _settings.Settings.Llm.SlashPopupPageSize, 3, 20, value => _settings.Settings.Llm.SlashPopupPageSize = value, normalizeOnInvalid: false))
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void TxtOverlayToolHookTimeoutMs_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
if (CommitOverlayNumericInput(TxtOverlayToolHookTimeoutMs, _settings.Settings.Llm.ToolHookTimeoutMs, 3000, 30000, value => _settings.Settings.Llm.ToolHookTimeoutMs = value, normalizeOnInvalid: false))
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void TxtOverlayMaxFavoriteSlashCommands_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
if (CommitOverlayNumericInput(TxtOverlayMaxFavoriteSlashCommands, _settings.Settings.Llm.MaxFavoriteSlashCommands, 1, 30, value => _settings.Settings.Llm.MaxFavoriteSlashCommands = value, normalizeOnInvalid: false))
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void TxtOverlayMaxRecentSlashCommands_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
if (CommitOverlayNumericInput(TxtOverlayMaxRecentSlashCommands, _settings.Settings.Llm.MaxRecentSlashCommands, 5, 50, value => _settings.Settings.Llm.MaxRecentSlashCommands = value, normalizeOnInvalid: false))
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void TxtOverlayPdfExportPath_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing || TxtOverlayPdfExportPath == null)
|
|
return;
|
|
|
|
_settings.Settings.Llm.PdfExportPath = TxtOverlayPdfExportPath.Text.Trim();
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void TxtOverlayPlanDiffMediumCount_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
if (CommitOverlayNumericInput(TxtOverlayPlanDiffMediumCount, _settings.Settings.Llm.PlanDiffSeverityMediumCount, 1, 999, value => _settings.Settings.Llm.PlanDiffSeverityMediumCount = value, normalizeOnInvalid: false))
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void TxtOverlayPlanDiffHighCount_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
if (CommitOverlayNumericInput(TxtOverlayPlanDiffHighCount, _settings.Settings.Llm.PlanDiffSeverityHighCount, 1, 999, value => _settings.Settings.Llm.PlanDiffSeverityHighCount = value, normalizeOnInvalid: false))
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void TxtOverlayPlanDiffMediumRatio_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
if (CommitOverlayNumericInput(TxtOverlayPlanDiffMediumRatio, _settings.Settings.Llm.PlanDiffSeverityMediumRatioPercent, 1, 100, value => _settings.Settings.Llm.PlanDiffSeverityMediumRatioPercent = value, normalizeOnInvalid: false))
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void TxtOverlayPlanDiffHighRatio_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
if (CommitOverlayNumericInput(TxtOverlayPlanDiffHighRatio, _settings.Settings.Llm.PlanDiffSeverityHighRatioPercent, 1, 100, value => _settings.Settings.Llm.PlanDiffSeverityHighRatioPercent = value, normalizeOnInvalid: false))
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void OverlayBrowseSkillFolderBtn_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
var dlg = new System.Windows.Forms.FolderBrowserDialog
|
|
{
|
|
Description = "스킬 파일이 있는 폴더를 선택하세요",
|
|
ShowNewFolderButton = true,
|
|
};
|
|
|
|
var current = _settings.Settings.Llm.SkillsFolderPath;
|
|
if (!string.IsNullOrWhiteSpace(current) && Directory.Exists(current))
|
|
dlg.SelectedPath = current;
|
|
|
|
if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK)
|
|
return;
|
|
|
|
_settings.Settings.Llm.SkillsFolderPath = dlg.SelectedPath;
|
|
SkillService.LoadSkills(dlg.SelectedPath, GetCurrentWorkFolder(), _settings.Settings.Llm.AdditionalSkillFolders);
|
|
RefreshOverlayEtcPanels();
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void OverlayOpenSkillFolderBtn_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
var folder = !string.IsNullOrWhiteSpace(_settings.Settings.Llm.SkillsFolderPath) && Directory.Exists(_settings.Settings.Llm.SkillsFolderPath)
|
|
? _settings.Settings.Llm.SkillsFolderPath
|
|
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills");
|
|
|
|
if (!Directory.Exists(folder))
|
|
Directory.CreateDirectory(folder);
|
|
|
|
try { System.Diagnostics.Process.Start("explorer.exe", folder); } catch { }
|
|
}
|
|
|
|
private void OverlayAddHookBtn_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
=> ShowOverlayHookEditDialog(null, -1);
|
|
|
|
private void OverlayOpenAuditLogBtn_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
// 클릭 효과 리셋
|
|
if (sender is Border border)
|
|
{
|
|
border.Opacity = 1.0;
|
|
border.RenderTransform = null;
|
|
}
|
|
try { System.Diagnostics.Process.Start("explorer.exe", Services.AuditLogService.GetAuditFolder()); } catch { }
|
|
}
|
|
|
|
private void BtnBrowsePdfExportPath_Click(object sender, MouseButtonEventArgs e)
|
|
{
|
|
// 클릭 효과 리셋
|
|
if (sender is Border border)
|
|
{
|
|
border.Opacity = 1.0;
|
|
border.RenderTransform = null;
|
|
}
|
|
|
|
var dlg = new System.Windows.Forms.FolderBrowserDialog
|
|
{
|
|
Description = "PDF 내보내기 기본 폴더를 선택하세요",
|
|
ShowNewFolderButton = true,
|
|
UseDescriptionForTitle = true,
|
|
};
|
|
|
|
var current = _settings.Settings.Llm.PdfExportPath;
|
|
if (!string.IsNullOrWhiteSpace(current) && Directory.Exists(current))
|
|
dlg.SelectedPath = current;
|
|
|
|
if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK)
|
|
return;
|
|
|
|
_settings.Settings.Llm.PdfExportPath = dlg.SelectedPath;
|
|
if (TxtOverlayPdfExportPath != null)
|
|
TxtOverlayPdfExportPath.Text = dlg.SelectedPath;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
/// <summary>설정 오버레이 액션 버튼 공통 호버/클릭 효과.</summary>
|
|
private void OverlayActionBtn_MouseEnter(object sender, MouseEventArgs e)
|
|
{
|
|
if (sender is Border border)
|
|
{
|
|
border.Opacity = 0.85;
|
|
border.Background = TryFindResource("ItemActiveBackground") as Brush
|
|
?? TryFindResource("ItemHoverBackground") as Brush
|
|
?? border.Background;
|
|
}
|
|
}
|
|
|
|
private void OverlayActionBtn_MouseLeave(object sender, MouseEventArgs e)
|
|
{
|
|
if (sender is Border border)
|
|
{
|
|
border.Opacity = 1.0;
|
|
border.Background = TryFindResource("ItemHoverBackground") as Brush ?? border.Background;
|
|
}
|
|
}
|
|
|
|
private void OverlayActionBtn_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (sender is Border border)
|
|
{
|
|
border.Opacity = 0.65;
|
|
border.RenderTransform = new ScaleTransform(0.96, 0.96);
|
|
border.RenderTransformOrigin = new Point(0.5, 0.5);
|
|
}
|
|
}
|
|
|
|
private void ChkOverlayEnableDragDropAiActions_Changed(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing || ChkOverlayEnableDragDropAiActions == null)
|
|
return;
|
|
|
|
_settings.Settings.Llm.EnableDragDropAiActions = ChkOverlayEnableDragDropAiActions.IsChecked == true;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void ChkOverlayDragDropAutoSend_Changed(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing || ChkOverlayDragDropAutoSend == null)
|
|
return;
|
|
|
|
_settings.Settings.Llm.DragDropAutoSend = ChkOverlayDragDropAutoSend.IsChecked == true;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void ChkOverlayWorkflowVisualizer_Changed(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing || ChkOverlayWorkflowVisualizer == null)
|
|
return;
|
|
|
|
_settings.Settings.Llm.WorkflowVisualizer = ChkOverlayWorkflowVisualizer.IsChecked == true;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void ChkOverlayShowTotalCallStats_Changed(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing || ChkOverlayShowTotalCallStats == null)
|
|
return;
|
|
|
|
_settings.Settings.Llm.ShowTotalCallStats = ChkOverlayShowTotalCallStats.IsChecked == true;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void ChkOverlayEnableAuditLog_Changed(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing || ChkOverlayEnableAuditLog == null)
|
|
return;
|
|
|
|
_settings.Settings.Llm.EnableAuditLog = ChkOverlayEnableAuditLog.IsChecked == true;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void ChkOverlayEnableDetailedLog_Changed(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing || ChkOverlayEnableDetailedLog == null)
|
|
return;
|
|
|
|
var enabled = ChkOverlayEnableDetailedLog.IsChecked == true;
|
|
_settings.Settings.Llm.EnableDetailedLog = enabled;
|
|
WorkflowLogService.IsEnabled = enabled;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void ChkOverlayEnableRawLlmLog_Changed(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing || ChkOverlayEnableRawLlmLog == null)
|
|
return;
|
|
|
|
var enabled = ChkOverlayEnableRawLlmLog.IsChecked == true;
|
|
_settings.Settings.Llm.EnableRawLlmLog = enabled;
|
|
WorkflowLogService.IsRawLogEnabled = enabled;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void ChkOverlayFeatureToggle_Changed(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
ApplyOverlaySettingsChanges(showToast: false, closeOverlay: false);
|
|
}
|
|
|
|
private void CmbOverlayMascotLevel_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
ApplyOverlaySettingsChanges(showToast: false, closeOverlay: false);
|
|
}
|
|
|
|
private void CmbOverlayCoworkOnComplete_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
ApplyOverlaySettingsChanges(showToast: false, closeOverlay: false);
|
|
}
|
|
|
|
private void CmbOverlayChatIconGlow_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing)
|
|
return;
|
|
|
|
ApplyOverlaySettingsChanges(showToast: false, closeOverlay: false);
|
|
}
|
|
|
|
private static void SelectComboBoxByTag(System.Windows.Controls.ComboBox? cmb, string? tag)
|
|
{
|
|
if (cmb == null) return;
|
|
foreach (var item in cmb.Items)
|
|
{
|
|
if (item is System.Windows.Controls.ComboBoxItem ci
|
|
&& string.Equals(ci.Tag?.ToString(), tag, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
cmb.SelectedItem = ci;
|
|
return;
|
|
}
|
|
}
|
|
if (cmb.Items.Count > 0)
|
|
cmb.SelectedIndex = 0; // fallback to "none"
|
|
}
|
|
|
|
private void OverlayNav_Checked(object sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is not RadioButton rb || rb.Tag is not string tag)
|
|
return;
|
|
|
|
SetOverlaySection(tag);
|
|
}
|
|
|
|
private void SetOverlaySection(string tag)
|
|
{
|
|
if (OverlaySectionService == null || OverlaySectionQuick == null || OverlaySectionDetail == null)
|
|
return;
|
|
|
|
var section = string.IsNullOrWhiteSpace(tag) ? "basic" : tag.Trim().ToLowerInvariant();
|
|
var showBasic = section == "basic";
|
|
var showChat = section == "chat";
|
|
var showShared = section == "shared";
|
|
var showCowork = section == "cowork";
|
|
var showCode = section == "code";
|
|
var showDev = section == "dev";
|
|
var showTools = section == "tools";
|
|
var showSkill = section == "skill";
|
|
var showBlock = section == "block";
|
|
|
|
OverlaySectionService.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed;
|
|
OverlaySectionQuick.Visibility = showShared ? Visibility.Visible : Visibility.Collapsed;
|
|
OverlaySectionDetail.Visibility = Visibility.Visible;
|
|
|
|
var headingTitle = section switch
|
|
{
|
|
"chat" => "채팅 설정",
|
|
"shared" => "코워크/코드 공통 설정",
|
|
"cowork" => "코워크 설정",
|
|
"code" => "코드 설정",
|
|
"dev" => "개발자 설정",
|
|
"tools" => "도구 설정",
|
|
"skill" => "스킬 설정",
|
|
"block" => "차단 설정",
|
|
_ => "공통 설정"
|
|
};
|
|
var headingDescription = section switch
|
|
{
|
|
"chat" => "Chat 탭에서 쓰는 입력/내보내기 같은 채팅 전용 설정입니다.",
|
|
"shared" => "Cowork와 Code에서 함께 쓰는 문맥/압축 관련 기본 설정입니다.",
|
|
"cowork" => "문서/업무 협업 흐름에 맞춘 코워크 전용 설정입니다.",
|
|
"code" => "코드 작업, 검증, 개발 도구 사용에 맞춘 설정입니다.",
|
|
"dev" => "실행 이력, 감사, 시각화 같은 개발자용 설정입니다.",
|
|
"tools" => "AX Agent가 사용할 도구와 훅 동작을 관리합니다.",
|
|
"skill" => "슬래시 스킬, 스킬 폴더, 폴백 모델, MCP 연결을 관리합니다.",
|
|
"block" => "에이전트가 접근하거나 수정하면 안 되는 경로와 형식을 관리합니다.",
|
|
_ => "Chat, Cowork, Code에서 공통으로 쓰는 기본 설정입니다."
|
|
};
|
|
|
|
if (OverlayTopHeadingTitle != null)
|
|
OverlayTopHeadingTitle.Text = headingTitle;
|
|
if (OverlayTopHeadingDescription != null)
|
|
OverlayTopHeadingDescription.Text = headingDescription;
|
|
if (OverlayAnchorCommon != null)
|
|
OverlayAnchorCommon.Text = headingTitle;
|
|
|
|
if (OverlayAiEnabledRow != null)
|
|
OverlayAiEnabledRow.Visibility = Visibility.Collapsed;
|
|
if (OverlayThemePanel != null)
|
|
OverlayThemePanel.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayThemeStylePanel != null)
|
|
OverlayThemeStylePanel.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayPdfExportPathRow != null)
|
|
OverlayPdfExportPathRow.Visibility = showChat ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleImageInput != null)
|
|
OverlayToggleImageInput.Visibility = showChat ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayModelEditorPanel != null)
|
|
OverlayModelEditorPanel.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayAnchorPermission != null)
|
|
OverlayAnchorPermission.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayTlsRow != null)
|
|
OverlayTlsRow.Visibility = showChat ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayAnchorAdvanced != null)
|
|
OverlayAnchorAdvanced.Visibility = showShared ? Visibility.Visible : Visibility.Collapsed;
|
|
if (TxtOverlayContextCompactTriggerPercent != null)
|
|
TxtOverlayContextCompactTriggerPercent.Visibility = Visibility.Collapsed;
|
|
if (OverlayMaxContextTokensRow != null)
|
|
OverlayMaxContextTokensRow.Visibility = showShared ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayTemperatureRow != null)
|
|
OverlayTemperatureRow.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayMaxRetryRow != null)
|
|
OverlayMaxRetryRow.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayMaxAgentIterationsRow != null)
|
|
OverlayMaxAgentIterationsRow.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayDeveloperRuntimePanel != null)
|
|
OverlayDeveloperRuntimePanel.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayDeveloperExtraPanel != null)
|
|
OverlayDeveloperExtraPanel.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayAdvancedTogglePanel != null)
|
|
OverlayAdvancedTogglePanel.Visibility = showDev || showCowork || showCode || showTools || showSkill ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToolsInfoPanel != null)
|
|
OverlayToolsInfoPanel.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToolsRuntimePanel != null)
|
|
OverlayToolsRuntimePanel.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToolRegistrySection != null)
|
|
OverlayToolRegistrySection.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlaySkillInfoPanel != null)
|
|
OverlaySkillInfoPanel.Visibility = showSkill ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlaySkillRuntimePanel != null)
|
|
OverlaySkillRuntimePanel.Visibility = showSkill ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayBlockInfoPanel != null)
|
|
OverlayBlockInfoPanel.Visibility = showBlock ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayBlockRuntimePanel != null)
|
|
OverlayBlockRuntimePanel.Visibility = showBlock ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleProactiveCompact != null)
|
|
OverlayToggleProactiveCompact.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleSkillSystem != null)
|
|
OverlayToggleSkillSystem.Visibility = showSkill ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleToolHooks != null)
|
|
OverlayToggleToolHooks.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleHookInputMutation != null)
|
|
OverlayToggleHookInputMutation.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleHookPermissionUpdate != null)
|
|
OverlayToggleHookPermissionUpdate.Visibility = showTools ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleCoworkVerification != null)
|
|
OverlayToggleCoworkVerification.Visibility = showCowork ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleCoworkOnComplete != null)
|
|
OverlayToggleCoworkOnComplete.Visibility = showCowork ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleAutoPreview != null)
|
|
OverlayToggleAutoPreview.Visibility = showCowork ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleCodeVerification != null)
|
|
OverlayToggleCodeVerification.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleCodeReview != null)
|
|
OverlayToggleCodeReview.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleParallelTools != null)
|
|
OverlayToggleParallelTools.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleProjectRules != null)
|
|
OverlayToggleProjectRules.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleAgentMemory != null)
|
|
OverlayToggleAgentMemory.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleIbmDiagnostic != null)
|
|
OverlayToggleIbmDiagnostic.Visibility = showDev ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleWorktreeTools != null)
|
|
OverlayToggleWorktreeTools.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleTeamTools != null)
|
|
OverlayToggleTeamTools.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleCronTools != null)
|
|
OverlayToggleCronTools.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlayToggleMascotCharacter != null)
|
|
OverlayToggleMascotCharacter.Visibility = showCode ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlaySectionGlowEffects != null)
|
|
OverlaySectionGlowEffects.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed;
|
|
if (OverlaySectionIconEffects != null)
|
|
OverlaySectionIconEffects.Visibility = showBasic ? Visibility.Visible : Visibility.Collapsed;
|
|
// V2 전환 완료 — OverlaySectionPlanViewer 제거됨
|
|
|
|
if (showTools || showSkill || showBlock)
|
|
RefreshOverlayEtcPanels();
|
|
}
|
|
|
|
private void RefreshOverlaySettingsPanel()
|
|
{
|
|
// 기본 컨트롤 상태만 동기적으로 설정 (빠름)
|
|
RefreshOverlayVisualState(loadDeferredInputs: true);
|
|
|
|
// 무거운 패널 빌드(스킬/MCP/도구 목록 등)는 UI 프레임 렌더링 후 비동기 지연 실행
|
|
// → 스트리밍 중 설정 열기 시 UI 프리즈 방지
|
|
Dispatcher.BeginInvoke(RefreshOverlayEtcPanels, System.Windows.Threading.DispatcherPriority.Background);
|
|
}
|
|
|
|
private void RefreshOverlayRetentionButtons()
|
|
{
|
|
// 대화 관리 섹션이 오버레이에서 제거됨 (설정 탭에서 관리)
|
|
}
|
|
|
|
private void ApplyOverlayRetentionButtonState(Button? button, bool selected)
|
|
{
|
|
if (button == null)
|
|
return;
|
|
|
|
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue;
|
|
var border = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray;
|
|
var hint = TryFindResource("HintBackground") as Brush ?? Brushes.Transparent;
|
|
var primary = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
|
var secondary = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
|
|
button.Background = selected ? hint : Brushes.Transparent;
|
|
button.BorderBrush = selected ? accent : border;
|
|
button.BorderThickness = new Thickness(1);
|
|
button.Foreground = selected ? accent : primary;
|
|
button.FontWeight = selected ? FontWeights.SemiBold : FontWeights.Normal;
|
|
button.Cursor = Cursors.Hand;
|
|
}
|
|
|
|
private void RefreshOverlayStorageSummary()
|
|
{
|
|
if (OverlayStorageSummaryText == null || OverlayStorageDriveText == null)
|
|
return;
|
|
|
|
var report = StorageAnalyzer.Analyze();
|
|
var appTotal = report.Conversations + report.AuditLogs + report.Logs + report.CodeIndex + report.EmbeddingDb + report.ClipboardHistory + report.Plugins + report.Skills + report.Settings;
|
|
OverlayStorageSummaryText.Text = $"앱 전체 사용량: {FormatStorageBytes(appTotal)}";
|
|
|
|
if (!string.IsNullOrWhiteSpace(report.DriveLabel) && report.DriveTotalSpace > 0)
|
|
{
|
|
var used = report.DriveTotalSpace - report.DriveFreeSpace;
|
|
var percent = report.DriveTotalSpace == 0 ? 0 : (int)Math.Round((double)used / report.DriveTotalSpace * 100);
|
|
OverlayStorageDriveText.Text = $"{report.DriveLabel} · 사용 {percent}% · 여유 {FormatStorageBytes(report.DriveFreeSpace)}";
|
|
}
|
|
else
|
|
{
|
|
OverlayStorageDriveText.Text = "로컬 앱 데이터 폴더 기준 사용량입니다.";
|
|
}
|
|
}
|
|
|
|
private void BtnOverlayRetention_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is not FrameworkElement element)
|
|
return;
|
|
|
|
var retainDays = element.Name switch
|
|
{
|
|
"BtnOverlayRetention7" => 7,
|
|
"BtnOverlayRetention30" => 30,
|
|
"BtnOverlayRetention90" => 90,
|
|
"BtnOverlayRetentionUnlimited" => 0,
|
|
_ => _settings.Settings.Llm.RetentionDays
|
|
};
|
|
|
|
_settings.Settings.Llm.RetentionDays = retainDays;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
RefreshOverlayRetentionButtons();
|
|
}
|
|
|
|
private void BtnOverlayStorageRefresh_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
RefreshOverlayStorageSummary();
|
|
}
|
|
|
|
private void BtnOverlayDeleteAllConversations_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
BtnDeleteAll_Click(sender, e);
|
|
RefreshOverlayStorageSummary();
|
|
}
|
|
|
|
private void BtnOverlayStorageCleanup_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var retainDays = Math.Max(0, _settings.Settings.Llm.RetentionDays);
|
|
var cleanedBytes = StorageAnalyzer.Cleanup(
|
|
retainDays,
|
|
cleanConversations: false,
|
|
cleanAuditLogs: true,
|
|
cleanLogs: true,
|
|
cleanCodeIndex: true,
|
|
cleanClipboard: true);
|
|
|
|
RefreshOverlayStorageSummary();
|
|
|
|
CustomMessageBox.Show(
|
|
cleanedBytes > 0
|
|
? $"저장 공간을 정리했습니다.\n확보된 공간: {StorageAnalyzer.FormatSize(cleanedBytes)}"
|
|
: "정리할 항목이 없었습니다.",
|
|
"저장 공간 정리",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Information);
|
|
}
|
|
|
|
private static string FormatStorageBytes(long bytes)
|
|
{
|
|
if (bytes >= 1024L * 1024 * 1024)
|
|
return $"{bytes / 1024.0 / 1024 / 1024:F1} GB";
|
|
if (bytes >= 1024L * 1024)
|
|
return $"{bytes / 1024.0 / 1024:F1} MB";
|
|
if (bytes >= 1024L)
|
|
return $"{bytes / 1024.0:F0} KB";
|
|
return $"{bytes} B";
|
|
}
|
|
|
|
private void RefreshOverlayEtcPanels()
|
|
{
|
|
var llm = _settings.Settings.Llm;
|
|
|
|
if (OverlaySkillsFolderPathText != null)
|
|
{
|
|
var defaultFolder = System.IO.Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
"AxCopilot",
|
|
"skills");
|
|
OverlaySkillsFolderPathText.Text = string.IsNullOrWhiteSpace(llm.SkillsFolderPath)
|
|
? defaultFolder
|
|
: llm.SkillsFolderPath.Trim();
|
|
}
|
|
|
|
if (TxtOverlaySlashPopupPageSize != null)
|
|
TxtOverlaySlashPopupPageSize.Text = Math.Clamp(llm.SlashPopupPageSize, 3, 20).ToString();
|
|
if (SldOverlaySlashPopupPageSize != null)
|
|
SldOverlaySlashPopupPageSize.Value = Math.Clamp(llm.SlashPopupPageSize, 3, 20);
|
|
if (TxtOverlaySlashPopupPageSizeValue != null)
|
|
TxtOverlaySlashPopupPageSizeValue.Text = Math.Clamp(llm.SlashPopupPageSize, 3, 20).ToString();
|
|
if (TxtOverlayToolHookTimeoutMs != null)
|
|
TxtOverlayToolHookTimeoutMs.Text = Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000).ToString();
|
|
if (SldOverlayToolHookTimeoutMs != null)
|
|
SldOverlayToolHookTimeoutMs.Value = Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000);
|
|
if (TxtOverlayToolHookTimeoutMsValue != null)
|
|
TxtOverlayToolHookTimeoutMsValue.Text = $"{Math.Clamp(llm.ToolHookTimeoutMs, 3000, 30000) / 1000}s";
|
|
if (TxtOverlayMaxFavoriteSlashCommands != null)
|
|
TxtOverlayMaxFavoriteSlashCommands.Text = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30).ToString();
|
|
if (SldOverlayMaxFavoriteSlashCommands != null)
|
|
SldOverlayMaxFavoriteSlashCommands.Value = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30);
|
|
if (TxtOverlayMaxFavoriteSlashCommandsValue != null)
|
|
TxtOverlayMaxFavoriteSlashCommandsValue.Text = Math.Clamp(llm.MaxFavoriteSlashCommands, 1, 30).ToString();
|
|
if (TxtOverlayMaxRecentSlashCommands != null)
|
|
TxtOverlayMaxRecentSlashCommands.Text = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50).ToString();
|
|
if (SldOverlayMaxRecentSlashCommands != null)
|
|
SldOverlayMaxRecentSlashCommands.Value = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50);
|
|
if (TxtOverlayMaxRecentSlashCommandsValue != null)
|
|
TxtOverlayMaxRecentSlashCommandsValue.Text = Math.Clamp(llm.MaxRecentSlashCommands, 5, 50).ToString();
|
|
if (ChkOverlayEnableDragDropAiActions != null)
|
|
ChkOverlayEnableDragDropAiActions.IsChecked = llm.EnableDragDropAiActions;
|
|
if (ChkOverlayDragDropAutoSend != null)
|
|
ChkOverlayDragDropAutoSend.IsChecked = llm.DragDropAutoSend;
|
|
|
|
BuildOverlayBlockedItems();
|
|
BuildOverlayHookCards();
|
|
BuildOverlaySkillListPanel();
|
|
BuildOverlayFallbackModelsPanel();
|
|
BuildOverlayMcpServerCards();
|
|
BuildOverlayToolRegistryPanel();
|
|
}
|
|
|
|
private void BuildOverlayBlockedItems()
|
|
{
|
|
if (OverlayBlockedPathsPanel != null)
|
|
{
|
|
OverlayBlockedPathsPanel.Children.Clear();
|
|
foreach (var path in _settings.Settings.Llm.BlockedPaths.Where(x => !string.IsNullOrWhiteSpace(x)))
|
|
{
|
|
OverlayBlockedPathsPanel.Children.Add(new Border
|
|
{
|
|
Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.White,
|
|
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(6),
|
|
Padding = new Thickness(8, 4, 8, 4),
|
|
Margin = new Thickness(0, 0, 0, 4),
|
|
Child = new TextBlock
|
|
{
|
|
Text = path,
|
|
FontSize = 11.5,
|
|
FontFamily = new FontFamily("Consolas, Malgun Gothic"),
|
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if (OverlayBlockedExtensionsPanel != null)
|
|
{
|
|
OverlayBlockedExtensionsPanel.Children.Clear();
|
|
foreach (var ext in _settings.Settings.Llm.BlockedExtensions.Where(x => !string.IsNullOrWhiteSpace(x)))
|
|
{
|
|
OverlayBlockedExtensionsPanel.Children.Add(new Border
|
|
{
|
|
Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.White,
|
|
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(6),
|
|
Padding = new Thickness(8, 4, 8, 4),
|
|
Margin = new Thickness(0, 0, 6, 6),
|
|
Child = new TextBlock
|
|
{
|
|
Text = ext,
|
|
FontSize = 11.5,
|
|
FontFamily = new FontFamily("Consolas, Malgun Gothic"),
|
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private void BuildOverlaySkillListPanel()
|
|
{
|
|
if (OverlaySkillListPanel == null)
|
|
return;
|
|
|
|
OverlaySkillListPanel.Children.Clear();
|
|
var skills = SkillService.Skills.ToList();
|
|
if (skills.Count == 0)
|
|
{
|
|
OverlaySkillListPanel.Children.Add(new TextBlock
|
|
{
|
|
Text = "로드된 스킬이 없습니다.",
|
|
FontSize = 11,
|
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray
|
|
});
|
|
return;
|
|
}
|
|
|
|
var unavailable = skills
|
|
.Where(skill => !skill.IsAvailable)
|
|
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
var autoSkills = SkillService.GetAutoSkills(_activeTab).ToList();
|
|
var directSkills = skills
|
|
.Where(skill => skill.IsAvailable
|
|
&& skill.UserInvocable
|
|
&& !autoSkills.Any(autoSkill => string.Equals(autoSkill.Name, skill.Name, StringComparison.OrdinalIgnoreCase)))
|
|
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
AddOverlaySkillSection("overlay-skill-direct", "직접 호출 스킬", "슬래시(/)로 직접 실행하는 스킬입니다.", directSkills, "#2563EB");
|
|
AddOverlaySkillSection("overlay-skill-auto", "자동/조건부 스킬", "조건에 따라 자동으로 붙거나 보조적으로 동작하는 스킬입니다.", autoSkills, "#0F766E");
|
|
AddOverlaySkillSection("overlay-skill-unavailable", "현재 사용 불가", "필요한 런타임이 없어 지금은 호출되지 않는 스킬입니다.", unavailable, "#9A3412");
|
|
}
|
|
|
|
private void AddOverlaySkillSection(string key, string title, string subtitle, List<SkillDefinition> skills, string accentHex)
|
|
{
|
|
if (OverlaySkillListPanel == null || skills.Count == 0)
|
|
return;
|
|
|
|
var body = new StackPanel();
|
|
foreach (var skill in skills)
|
|
{
|
|
var card = new Border
|
|
{
|
|
Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.White,
|
|
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(8),
|
|
Padding = new Thickness(10, 8, 10, 8),
|
|
Margin = new Thickness(0, 0, 0, 6),
|
|
};
|
|
|
|
var stack = new StackPanel();
|
|
stack.Children.Add(new TextBlock
|
|
{
|
|
Text = "/" + skill.Name,
|
|
FontSize = 12.5,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black
|
|
});
|
|
|
|
if (!string.IsNullOrWhiteSpace(skill.Label) &&
|
|
!string.Equals(skill.Label.Trim(), skill.Name, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
stack.Children.Add(new TextBlock
|
|
{
|
|
Text = skill.Label.Trim(),
|
|
Margin = new Thickness(0, 3, 0, 0),
|
|
FontSize = 11,
|
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray
|
|
});
|
|
}
|
|
|
|
stack.Children.Add(new TextBlock
|
|
{
|
|
Text = string.IsNullOrWhiteSpace(skill.Description)
|
|
? (string.IsNullOrWhiteSpace(skill.Label) ? skill.Name : skill.Label)
|
|
: skill.Description,
|
|
Margin = new Thickness(0, 4, 0, 0),
|
|
FontSize = 11,
|
|
TextWrapping = TextWrapping.Wrap,
|
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray
|
|
});
|
|
|
|
card.Child = stack;
|
|
body.Children.Add(card);
|
|
}
|
|
|
|
OverlaySkillListPanel.Children.Add(CreateOverlayCollapsibleSection(
|
|
key,
|
|
$"{title} ({skills.Count})",
|
|
subtitle,
|
|
body,
|
|
defaultExpanded: false,
|
|
accentHex: accentHex));
|
|
}
|
|
|
|
private void BuildOverlayFallbackModelsPanel()
|
|
{
|
|
if (OverlayFallbackModelsPanel == null)
|
|
return;
|
|
|
|
OverlayFallbackModelsPanel.Children.Clear();
|
|
var llm = _settings.Settings.Llm;
|
|
var sections = new[]
|
|
{
|
|
("ollama", "Ollama"),
|
|
("vllm", "vLLM"),
|
|
("gemini", "Gemini"),
|
|
("claude", "Claude")
|
|
};
|
|
|
|
foreach (var (service, label) in sections)
|
|
{
|
|
var candidates = GetModelCandidates(service);
|
|
OverlayFallbackModelsPanel.Children.Add(new TextBlock
|
|
{
|
|
Text = label,
|
|
FontSize = 11.5,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
|
|
Margin = new Thickness(0, 0, 0, 6)
|
|
});
|
|
|
|
if (candidates.Count == 0)
|
|
{
|
|
OverlayFallbackModelsPanel.Children.Add(new TextBlock
|
|
{
|
|
Text = "등록된 모델 없음",
|
|
FontSize = 10.5,
|
|
Margin = new Thickness(8, 0, 0, 8),
|
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray
|
|
});
|
|
continue;
|
|
}
|
|
|
|
foreach (var candidate in candidates)
|
|
{
|
|
var enabled = llm.FallbackModels.Any(x => x.Equals(candidate.Id, StringComparison.OrdinalIgnoreCase));
|
|
var grid = new Grid();
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition());
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
|
|
|
var nameText = new TextBlock
|
|
{
|
|
Text = candidate.Label,
|
|
FontSize = 11.5,
|
|
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black
|
|
};
|
|
var stateText = new TextBlock
|
|
{
|
|
Text = enabled ? "사용" : "미사용",
|
|
FontSize = 10.5,
|
|
Foreground = enabled
|
|
? (TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue)
|
|
: (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray),
|
|
Margin = new Thickness(12, 0, 0, 0)
|
|
};
|
|
Grid.SetColumn(stateText, 1);
|
|
grid.Children.Add(nameText);
|
|
grid.Children.Add(stateText);
|
|
|
|
OverlayFallbackModelsPanel.Children.Add(new Border
|
|
{
|
|
Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.White,
|
|
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(8),
|
|
Padding = new Thickness(10, 7, 10, 7),
|
|
Margin = new Thickness(0, 0, 0, 6),
|
|
Child = grid
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private void BuildOverlayMcpServerCards()
|
|
{
|
|
if (OverlayMcpServerListPanel == null)
|
|
return;
|
|
|
|
OverlayMcpServerListPanel.Children.Clear();
|
|
var servers = _settings.Settings.Llm.McpServers;
|
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue;
|
|
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray;
|
|
var itemBackground = TryFindResource("ItemBackground") as Brush ?? Brushes.White;
|
|
var cardBackground = TryFindResource("LauncherBackground") as Brush ?? Brushes.White;
|
|
|
|
if (servers == null || servers.Count == 0)
|
|
{
|
|
OverlayMcpServerListPanel.Children.Add(new TextBlock
|
|
{
|
|
Text = "등록된 MCP 서버가 없습니다.",
|
|
FontSize = 11,
|
|
Foreground = secondaryText
|
|
});
|
|
return;
|
|
}
|
|
|
|
Border CreateActionChip(string text, Brush foreground, Action onClick)
|
|
{
|
|
var border = new Border
|
|
{
|
|
Background = Brushes.Transparent,
|
|
BorderBrush = Brushes.Transparent,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(8),
|
|
Padding = new Thickness(8, 4, 8, 4),
|
|
Margin = new Thickness(6, 0, 0, 0),
|
|
Cursor = Cursors.Hand,
|
|
};
|
|
|
|
var label = new TextBlock
|
|
{
|
|
Text = text,
|
|
FontSize = 11,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = foreground,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
};
|
|
|
|
border.Child = label;
|
|
border.MouseEnter += (_, _) =>
|
|
{
|
|
border.Background = TryFindResource("HintBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(20, 0, 0, 0));
|
|
border.BorderBrush = borderBrush;
|
|
};
|
|
border.MouseLeave += (_, _) =>
|
|
{
|
|
border.Background = Brushes.Transparent;
|
|
border.BorderBrush = Brushes.Transparent;
|
|
};
|
|
border.MouseLeftButtonUp += (_, _) => onClick();
|
|
return border;
|
|
}
|
|
|
|
for (int index = 0; index < servers.Count; index++)
|
|
{
|
|
var server = servers[index];
|
|
var card = new Border
|
|
{
|
|
Background = cardBackground,
|
|
BorderBrush = borderBrush,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(8),
|
|
Padding = new Thickness(12, 10, 12, 10),
|
|
Margin = new Thickness(0, 0, 0, 6)
|
|
};
|
|
|
|
var root = new StackPanel();
|
|
var header = new Grid();
|
|
header.ColumnDefinitions.Add(new ColumnDefinition());
|
|
header.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
|
|
|
var title = new TextBlock
|
|
{
|
|
Text = server.Name,
|
|
FontSize = 12,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = primaryText,
|
|
VerticalAlignment = VerticalAlignment.Center
|
|
};
|
|
header.Children.Add(title);
|
|
|
|
var actions = new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
};
|
|
actions.Children.Add(CreateActionChip(server.Enabled ? "비활성화" : "활성화", accentBrush, () =>
|
|
{
|
|
_settings.Settings.Llm.McpServers[index].Enabled = !_settings.Settings.Llm.McpServers[index].Enabled;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
BuildOverlayMcpServerCards();
|
|
}));
|
|
actions.Children.Add(CreateActionChip("삭제", BrushFromHex("#DC2626"), () =>
|
|
{
|
|
var result = CustomMessageBox.Show($"'{server.Name}' 서버를 삭제하시겠습니까?", "MCP 서버 삭제",
|
|
MessageBoxButton.YesNo, MessageBoxImage.Question);
|
|
if (result != MessageBoxResult.Yes)
|
|
return;
|
|
|
|
_settings.Settings.Llm.McpServers.RemoveAt(index);
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
BuildOverlayMcpServerCards();
|
|
}));
|
|
Grid.SetColumn(actions, 1);
|
|
header.Children.Add(actions);
|
|
root.Children.Add(header);
|
|
|
|
var commandCard = new Border
|
|
{
|
|
Background = itemBackground,
|
|
BorderBrush = borderBrush,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(8),
|
|
Padding = new Thickness(10, 8, 10, 8),
|
|
Margin = new Thickness(0, 8, 0, 0),
|
|
Child = new StackPanel
|
|
{
|
|
Children =
|
|
{
|
|
new TextBlock
|
|
{
|
|
Text = server.Command,
|
|
FontSize = 10.8,
|
|
TextWrapping = TextWrapping.Wrap,
|
|
Foreground = primaryText,
|
|
},
|
|
new TextBlock
|
|
{
|
|
Text = server.Enabled ? "활성 상태" : "비활성 상태",
|
|
Margin = new Thickness(0, 4, 0, 0),
|
|
FontSize = 10.5,
|
|
Foreground = secondaryText,
|
|
}
|
|
}
|
|
}
|
|
};
|
|
root.Children.Add(commandCard);
|
|
card.Child = root;
|
|
OverlayMcpServerListPanel.Children.Add(card);
|
|
}
|
|
}
|
|
|
|
private void BtnOverlayAddMcpServer_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var nameDialog = new InputDialog("MCP 서버 추가", "서버 이름:", placeholder: "예: my-mcp-server")
|
|
{
|
|
Owner = this
|
|
};
|
|
if (nameDialog.ShowDialog() != true || string.IsNullOrWhiteSpace(nameDialog.ResponseText))
|
|
return;
|
|
|
|
var commandDialog = new InputDialog("MCP 서버 추가", "실행 명령:", placeholder: "예: npx -y @modelcontextprotocol/server-filesystem")
|
|
{
|
|
Owner = this
|
|
};
|
|
if (commandDialog.ShowDialog() != true || string.IsNullOrWhiteSpace(commandDialog.ResponseText))
|
|
return;
|
|
|
|
_settings.Settings.Llm.McpServers.Add(new Models.McpServerEntry
|
|
{
|
|
Name = nameDialog.ResponseText.Trim(),
|
|
Command = commandDialog.ResponseText.Trim(),
|
|
Enabled = true,
|
|
});
|
|
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
BuildOverlayMcpServerCards();
|
|
}
|
|
|
|
private void BuildOverlayToolRegistryPanel()
|
|
{
|
|
if (OverlayToolRegistryPanel == null)
|
|
return;
|
|
|
|
OverlayToolRegistryPanel.Children.Clear();
|
|
var grouped = _toolRegistry.All
|
|
.GroupBy(tool => GetOverlayToolCategory(tool.Name))
|
|
.OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
foreach (var group in grouped)
|
|
{
|
|
var body = new StackPanel();
|
|
foreach (var tool in group.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
var isDisabled = _settings.Settings.Llm.DisabledTools.Contains(tool.Name, StringComparer.OrdinalIgnoreCase);
|
|
var row = new Border
|
|
{
|
|
Background = TryFindResource("LauncherBackground") as Brush ?? Brushes.White,
|
|
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(8),
|
|
Padding = new Thickness(10, 8, 10, 8),
|
|
Margin = new Thickness(0, 0, 0, 6),
|
|
};
|
|
|
|
var grid = new Grid();
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition());
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
|
|
|
var rowStack = new StackPanel { Margin = new Thickness(0, 0, 12, 0) };
|
|
rowStack.Children.Add(new TextBlock
|
|
{
|
|
Text = tool.Name,
|
|
FontSize = 12,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = isDisabled
|
|
? (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray)
|
|
: (TryFindResource("PrimaryText") as Brush ?? Brushes.Black)
|
|
});
|
|
rowStack.Children.Add(new TextBlock
|
|
{
|
|
Text = (isDisabled ? "비활성" : "활성") + " · " + tool.Description,
|
|
Margin = new Thickness(0, 4, 0, 0),
|
|
FontSize = 10.8,
|
|
TextWrapping = TextWrapping.Wrap,
|
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray
|
|
});
|
|
grid.Children.Add(rowStack);
|
|
|
|
var toggle = new CheckBox
|
|
{
|
|
IsChecked = !isDisabled,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Style = TryFindResource("ToggleSwitch") as Style,
|
|
};
|
|
toggle.Checked += (_, _) =>
|
|
{
|
|
_settings.Settings.Llm.DisabledTools.RemoveAll(name => string.Equals(name, tool.Name, StringComparison.OrdinalIgnoreCase));
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
RefreshOverlayEtcPanels();
|
|
};
|
|
toggle.Unchecked += (_, _) =>
|
|
{
|
|
if (!_settings.Settings.Llm.DisabledTools.Contains(tool.Name, StringComparer.OrdinalIgnoreCase))
|
|
_settings.Settings.Llm.DisabledTools.Add(tool.Name);
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
RefreshOverlayEtcPanels();
|
|
};
|
|
Grid.SetColumn(toggle, 1);
|
|
grid.Children.Add(toggle);
|
|
|
|
row.Child = grid;
|
|
body.Children.Add(row);
|
|
}
|
|
|
|
OverlayToolRegistryPanel.Children.Add(CreateOverlayCollapsibleSection(
|
|
"overlay-tool-" + group.Key,
|
|
$"{group.Key} ({group.Count()})",
|
|
"카테고리별 도구 목록입니다. 펼치면 상세 이름과 사용 여부를 바로 바꿀 수 있습니다.",
|
|
body,
|
|
defaultExpanded: false,
|
|
accentHex: "#4F46E5"));
|
|
}
|
|
}
|
|
|
|
private void BuildOverlayHookCards()
|
|
{
|
|
if (OverlayHookListPanel == null)
|
|
return;
|
|
|
|
OverlayHookListPanel.Children.Clear();
|
|
|
|
var hooks = _settings.Settings.Llm.AgentHooks;
|
|
if (hooks.Count == 0)
|
|
{
|
|
OverlayHookListPanel.Children.Add(new TextBlock
|
|
{
|
|
Text = "등록된 훅이 없습니다.",
|
|
FontSize = 11,
|
|
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray
|
|
});
|
|
return;
|
|
}
|
|
|
|
var body = new StackPanel();
|
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue;
|
|
|
|
for (int i = 0; i < hooks.Count; i++)
|
|
{
|
|
var hook = hooks[i];
|
|
var idx = i;
|
|
var card = new Border
|
|
{
|
|
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke,
|
|
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(8),
|
|
Padding = new Thickness(10, 8, 10, 8),
|
|
Margin = new Thickness(0, 0, 0, 6),
|
|
};
|
|
|
|
var grid = new Grid();
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition());
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
|
|
|
var toggle = new CheckBox
|
|
{
|
|
IsChecked = hook.Enabled,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(0, 0, 8, 0),
|
|
Style = TryFindResource("ToggleSwitch") as Style,
|
|
};
|
|
toggle.Checked += (_, _) =>
|
|
{
|
|
hook.Enabled = true;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
};
|
|
toggle.Unchecked += (_, _) =>
|
|
{
|
|
hook.Enabled = false;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
};
|
|
Grid.SetColumn(toggle, 0);
|
|
grid.Children.Add(toggle);
|
|
|
|
var info = new StackPanel();
|
|
var header = new StackPanel { Orientation = Orientation.Horizontal };
|
|
header.Children.Add(new TextBlock
|
|
{
|
|
Text = hook.Name,
|
|
FontSize = 12.5,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = primaryText,
|
|
});
|
|
header.Children.Add(new Border
|
|
{
|
|
Background = BrushFromHex(hook.Timing == "pre" ? "#FFEDD5" : "#DCFCE7"),
|
|
BorderBrush = BrushFromHex(hook.Timing == "pre" ? "#FDBA74" : "#86EFAC"),
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(5),
|
|
Padding = new Thickness(5, 1, 5, 1),
|
|
Margin = new Thickness(6, 0, 0, 0),
|
|
Child = new TextBlock
|
|
{
|
|
Text = hook.Timing == "pre" ? "PRE" : "POST",
|
|
FontSize = 9.5,
|
|
FontWeight = FontWeights.Bold,
|
|
Foreground = BrushFromHex(hook.Timing == "pre" ? "#9A3412" : "#166534"),
|
|
}
|
|
});
|
|
if (!string.IsNullOrWhiteSpace(hook.ToolName) && hook.ToolName != "*")
|
|
{
|
|
header.Children.Add(new Border
|
|
{
|
|
Background = BrushFromHex("#EEF2FF"),
|
|
BorderBrush = BrushFromHex("#C7D2FE"),
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(5),
|
|
Padding = new Thickness(5, 1, 5, 1),
|
|
Margin = new Thickness(6, 0, 0, 0),
|
|
Child = new TextBlock
|
|
{
|
|
Text = hook.ToolName,
|
|
FontSize = 9.5,
|
|
Foreground = BrushFromHex("#3730A3"),
|
|
}
|
|
});
|
|
}
|
|
info.Children.Add(header);
|
|
info.Children.Add(new TextBlock
|
|
{
|
|
Text = Path.GetFileName(hook.ScriptPath),
|
|
Margin = new Thickness(0, 4, 0, 0),
|
|
FontSize = 11,
|
|
Foreground = secondaryText
|
|
});
|
|
if (!string.IsNullOrWhiteSpace(hook.Arguments))
|
|
{
|
|
info.Children.Add(new TextBlock
|
|
{
|
|
Text = hook.Arguments,
|
|
Margin = new Thickness(0, 3, 0, 0),
|
|
FontSize = 10.5,
|
|
TextWrapping = TextWrapping.Wrap,
|
|
Foreground = secondaryText
|
|
});
|
|
}
|
|
Grid.SetColumn(info, 1);
|
|
grid.Children.Add(info);
|
|
|
|
var editBtn = new Border
|
|
{
|
|
Cursor = Cursors.Hand,
|
|
Padding = new Thickness(6),
|
|
Margin = new Thickness(6, 0, 2, 0),
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Child = new TextBlock
|
|
{
|
|
Text = "\uE70F",
|
|
FontFamily = s_segoeIconFont,
|
|
FontSize = 12,
|
|
Foreground = accentBrush,
|
|
}
|
|
};
|
|
editBtn.MouseLeftButtonUp += (_, _) => ShowOverlayHookEditDialog(hooks[idx], idx);
|
|
Grid.SetColumn(editBtn, 2);
|
|
grid.Children.Add(editBtn);
|
|
|
|
var deleteBtn = new Border
|
|
{
|
|
Cursor = Cursors.Hand,
|
|
Padding = new Thickness(6),
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Child = new TextBlock
|
|
{
|
|
Text = "\uE74D",
|
|
FontFamily = s_segoeIconFont,
|
|
FontSize = 12,
|
|
Foreground = BrushFromHex("#DC2626"),
|
|
}
|
|
};
|
|
deleteBtn.MouseLeftButtonUp += (_, _) =>
|
|
{
|
|
hooks.RemoveAt(idx);
|
|
BuildOverlayHookCards();
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
};
|
|
Grid.SetColumn(deleteBtn, 3);
|
|
grid.Children.Add(deleteBtn);
|
|
|
|
card.Child = grid;
|
|
body.Children.Add(card);
|
|
}
|
|
|
|
OverlayHookListPanel.Children.Add(CreateOverlayCollapsibleSection(
|
|
"overlay-hooks",
|
|
$"등록된 훅 ({hooks.Count})",
|
|
"도구 실행 전후에 연결되는 스크립트입니다.",
|
|
body,
|
|
defaultExpanded: false,
|
|
accentHex: "#0F766E"));
|
|
}
|
|
|
|
private Border CreateOverlayCollapsibleSection(string key, string title, string subtitle, UIElement content, bool defaultExpanded, string accentHex)
|
|
{
|
|
var itemBackground = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke;
|
|
var launcherBackground = TryFindResource("LauncherBackground") as Brush ?? Brushes.White;
|
|
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray;
|
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var expanded = _overlaySectionExpandedStates.TryGetValue(key, out var stored) ? stored : defaultExpanded;
|
|
|
|
var bodyBorder = new Border
|
|
{
|
|
Margin = new Thickness(0, 10, 0, 0),
|
|
Child = content,
|
|
Visibility = expanded ? Visibility.Visible : Visibility.Collapsed
|
|
};
|
|
|
|
var caret = new TextBlock
|
|
{
|
|
Text = expanded ? "\uE70D" : "\uE76C",
|
|
FontFamily = s_segoeIconFont,
|
|
FontSize = 11,
|
|
Foreground = secondaryText,
|
|
VerticalAlignment = VerticalAlignment.Center
|
|
};
|
|
|
|
var headerGrid = new Grid();
|
|
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
|
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
|
|
|
var headerText = new StackPanel();
|
|
headerText.Children.Add(new TextBlock
|
|
{
|
|
Text = title,
|
|
FontSize = 12.5,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = primaryText
|
|
});
|
|
if (!string.IsNullOrWhiteSpace(subtitle))
|
|
{
|
|
headerText.Children.Add(new TextBlock
|
|
{
|
|
Text = subtitle,
|
|
Margin = new Thickness(0, 4, 0, 0),
|
|
FontSize = 10.8,
|
|
TextWrapping = TextWrapping.Wrap,
|
|
Foreground = secondaryText
|
|
});
|
|
}
|
|
headerGrid.Children.Add(headerText);
|
|
Grid.SetColumn(caret, 1);
|
|
headerGrid.Children.Add(caret);
|
|
|
|
var headerBorder = new Border
|
|
{
|
|
Background = launcherBackground,
|
|
BorderBrush = BrushFromHex(accentHex),
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(8),
|
|
Padding = new Thickness(10, 8, 10, 8),
|
|
Cursor = Cursors.Hand,
|
|
Child = headerGrid
|
|
};
|
|
|
|
void Toggle()
|
|
{
|
|
var nextExpanded = bodyBorder.Visibility != Visibility.Visible;
|
|
bodyBorder.Visibility = nextExpanded ? Visibility.Visible : Visibility.Collapsed;
|
|
caret.Text = nextExpanded ? "\uE70D" : "\uE76C";
|
|
_overlaySectionExpandedStates[key] = nextExpanded;
|
|
}
|
|
|
|
headerBorder.MouseLeftButtonUp += (_, _) => Toggle();
|
|
|
|
return new Border
|
|
{
|
|
Background = itemBackground,
|
|
BorderBrush = borderBrush,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(10),
|
|
Padding = new Thickness(10),
|
|
Margin = new Thickness(0, 0, 0, 8),
|
|
Child = new StackPanel
|
|
{
|
|
Children =
|
|
{
|
|
headerBorder,
|
|
bodyBorder
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
private static TextBlock CreateOverlayPlaceholder(string text, Brush foreground, string? currentValue)
|
|
{
|
|
return new TextBlock
|
|
{
|
|
Text = text,
|
|
FontSize = 13,
|
|
Foreground = foreground,
|
|
Opacity = 0.45,
|
|
IsHitTestVisible = false,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Padding = new Thickness(14, 8, 14, 8),
|
|
Visibility = string.IsNullOrEmpty(currentValue) ? Visibility.Visible : Visibility.Collapsed,
|
|
};
|
|
}
|
|
|
|
private void ShowOverlayHookEditDialog(AgentHookEntry? existing, int index)
|
|
{
|
|
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? BrushFromHex("#FFFFFF");
|
|
var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
|
var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.LightGray;
|
|
var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke;
|
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
|
|
|
var isNew = existing == null;
|
|
var dlg = new Window
|
|
{
|
|
Title = isNew ? "훅 추가" : "훅 편집",
|
|
Width = 420,
|
|
SizeToContent = SizeToContent.Height,
|
|
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
|
Owner = this,
|
|
ResizeMode = ResizeMode.NoResize,
|
|
WindowStyle = WindowStyle.None,
|
|
AllowsTransparency = true,
|
|
Background = Brushes.Transparent,
|
|
ShowInTaskbar = false
|
|
};
|
|
|
|
var border = new Border
|
|
{
|
|
Background = bgBrush,
|
|
CornerRadius = new CornerRadius(12),
|
|
BorderBrush = borderBrush,
|
|
BorderThickness = new Thickness(1),
|
|
Padding = new Thickness(20)
|
|
};
|
|
var stack = new StackPanel();
|
|
stack.Children.Add(new TextBlock
|
|
{
|
|
Text = isNew ? "훅 추가" : "훅 편집",
|
|
FontSize = 15,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = fgBrush,
|
|
Margin = new Thickness(0, 0, 0, 14)
|
|
});
|
|
|
|
dlg.KeyDown += (_, e) => { if (e.Key == Key.Escape) dlg.Close(); };
|
|
|
|
stack.Children.Add(new TextBlock { Text = "훅 이름", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 4) });
|
|
var nameBox = new TextBox
|
|
{
|
|
Text = existing?.Name ?? "",
|
|
FontSize = 13,
|
|
Foreground = fgBrush,
|
|
Background = itemBg,
|
|
BorderBrush = borderBrush,
|
|
Padding = new Thickness(12, 8, 12, 8)
|
|
};
|
|
var nameHolder = CreateOverlayPlaceholder("예: 코드 리뷰 후 알림", subFgBrush, existing?.Name);
|
|
nameBox.TextChanged += (_, _) => nameHolder.Visibility = string.IsNullOrEmpty(nameBox.Text) ? Visibility.Visible : Visibility.Collapsed;
|
|
var nameGrid = new Grid();
|
|
nameGrid.Children.Add(nameBox);
|
|
nameGrid.Children.Add(nameHolder);
|
|
stack.Children.Add(nameGrid);
|
|
|
|
stack.Children.Add(new TextBlock { Text = "대상 도구 (* = 모든 도구)", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 10, 0, 4) });
|
|
var toolBox = new TextBox
|
|
{
|
|
Text = existing?.ToolName ?? "*",
|
|
FontSize = 13,
|
|
Foreground = fgBrush,
|
|
Background = itemBg,
|
|
BorderBrush = borderBrush,
|
|
Padding = new Thickness(12, 8, 12, 8)
|
|
};
|
|
var toolHolder = CreateOverlayPlaceholder("예: file_write, grep_tool", subFgBrush, existing?.ToolName ?? "*");
|
|
toolBox.TextChanged += (_, _) => toolHolder.Visibility = string.IsNullOrEmpty(toolBox.Text) ? Visibility.Visible : Visibility.Collapsed;
|
|
var toolGrid = new Grid();
|
|
toolGrid.Children.Add(toolBox);
|
|
toolGrid.Children.Add(toolHolder);
|
|
stack.Children.Add(toolGrid);
|
|
|
|
stack.Children.Add(new TextBlock { Text = "실행 타이밍", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 10, 0, 4) });
|
|
var timingPanel = new StackPanel { Orientation = Orientation.Horizontal };
|
|
var preRadio = new RadioButton
|
|
{
|
|
Content = "Pre (실행 전)",
|
|
Foreground = fgBrush,
|
|
FontSize = 13,
|
|
Margin = new Thickness(0, 0, 16, 0),
|
|
IsChecked = (existing?.Timing ?? "post") == "pre"
|
|
};
|
|
var postRadio = new RadioButton
|
|
{
|
|
Content = "Post (실행 후)",
|
|
Foreground = fgBrush,
|
|
FontSize = 13,
|
|
IsChecked = (existing?.Timing ?? "post") != "pre"
|
|
};
|
|
timingPanel.Children.Add(preRadio);
|
|
timingPanel.Children.Add(postRadio);
|
|
stack.Children.Add(timingPanel);
|
|
|
|
stack.Children.Add(new TextBlock { Text = "스크립트 경로 (.bat / .cmd / .ps1)", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 10, 0, 4) });
|
|
var pathGrid = new Grid();
|
|
pathGrid.ColumnDefinitions.Add(new ColumnDefinition());
|
|
pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
|
var pathInnerGrid = new Grid();
|
|
var pathBox = new TextBox
|
|
{
|
|
Text = existing?.ScriptPath ?? "",
|
|
FontSize = 13,
|
|
Foreground = fgBrush,
|
|
Background = itemBg,
|
|
BorderBrush = borderBrush,
|
|
Padding = new Thickness(12, 8, 12, 8)
|
|
};
|
|
var pathHolder = CreateOverlayPlaceholder("예: C:\\scripts\\review-notify.bat", subFgBrush, existing?.ScriptPath);
|
|
pathBox.TextChanged += (_, _) => pathHolder.Visibility = string.IsNullOrEmpty(pathBox.Text) ? Visibility.Visible : Visibility.Collapsed;
|
|
pathInnerGrid.Children.Add(pathBox);
|
|
pathInnerGrid.Children.Add(pathHolder);
|
|
pathGrid.Children.Add(pathInnerGrid);
|
|
|
|
var browseBtn = new Border
|
|
{
|
|
Background = itemBg,
|
|
CornerRadius = new CornerRadius(6),
|
|
Padding = new Thickness(10, 6, 10, 6),
|
|
Margin = new Thickness(6, 0, 0, 0),
|
|
Cursor = Cursors.Hand,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Child = new TextBlock
|
|
{
|
|
Text = "...",
|
|
FontSize = 13,
|
|
Foreground = accentBrush
|
|
}
|
|
};
|
|
browseBtn.MouseLeftButtonUp += (_, _) =>
|
|
{
|
|
var ofd = new OpenFileDialog
|
|
{
|
|
Filter = "스크립트 파일|*.bat;*.cmd;*.ps1|모든 파일|*.*",
|
|
Title = "훅 스크립트 선택",
|
|
};
|
|
if (ofd.ShowDialog() == true)
|
|
pathBox.Text = ofd.FileName;
|
|
};
|
|
Grid.SetColumn(browseBtn, 1);
|
|
pathGrid.Children.Add(browseBtn);
|
|
stack.Children.Add(pathGrid);
|
|
|
|
stack.Children.Add(new TextBlock { Text = "추가 인수 (선택)", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 10, 0, 4) });
|
|
var argsBox = new TextBox
|
|
{
|
|
Text = existing?.Arguments ?? "",
|
|
FontSize = 13,
|
|
Foreground = fgBrush,
|
|
Background = itemBg,
|
|
BorderBrush = borderBrush,
|
|
Padding = new Thickness(12, 8, 12, 8)
|
|
};
|
|
var argsHolder = CreateOverlayPlaceholder("예: --verbose --output log.txt", subFgBrush, existing?.Arguments);
|
|
argsBox.TextChanged += (_, _) => argsHolder.Visibility = string.IsNullOrEmpty(argsBox.Text) ? Visibility.Visible : Visibility.Collapsed;
|
|
var argsGrid = new Grid();
|
|
argsGrid.Children.Add(argsBox);
|
|
argsGrid.Children.Add(argsHolder);
|
|
stack.Children.Add(argsGrid);
|
|
|
|
var btnRow = new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
Margin = new Thickness(0, 16, 0, 0)
|
|
};
|
|
var cancelBorder = new Border
|
|
{
|
|
Background = itemBg,
|
|
CornerRadius = new CornerRadius(8),
|
|
Padding = new Thickness(16, 8, 16, 8),
|
|
Margin = new Thickness(0, 0, 8, 0),
|
|
Cursor = Cursors.Hand,
|
|
Child = new TextBlock { Text = "취소", FontSize = 13, Foreground = subFgBrush }
|
|
};
|
|
cancelBorder.MouseLeftButtonUp += (_, _) => dlg.Close();
|
|
btnRow.Children.Add(cancelBorder);
|
|
|
|
var saveBorder = new Border
|
|
{
|
|
Background = accentBrush,
|
|
CornerRadius = new CornerRadius(8),
|
|
Padding = new Thickness(16, 8, 16, 8),
|
|
Cursor = Cursors.Hand,
|
|
Child = new TextBlock
|
|
{
|
|
Text = isNew ? "추가" : "저장",
|
|
FontSize = 13,
|
|
Foreground = Brushes.White,
|
|
FontWeight = FontWeights.SemiBold
|
|
}
|
|
};
|
|
saveBorder.MouseLeftButtonUp += (_, _) =>
|
|
{
|
|
if (string.IsNullOrWhiteSpace(nameBox.Text) || string.IsNullOrWhiteSpace(pathBox.Text))
|
|
{
|
|
CustomMessageBox.Show("훅 이름과 스크립트 경로를 입력하세요.", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
var entry = new AgentHookEntry
|
|
{
|
|
Name = nameBox.Text.Trim(),
|
|
ToolName = string.IsNullOrWhiteSpace(toolBox.Text) ? "*" : toolBox.Text.Trim(),
|
|
Timing = preRadio.IsChecked == true ? "pre" : "post",
|
|
ScriptPath = pathBox.Text.Trim(),
|
|
Arguments = argsBox.Text.Trim(),
|
|
Enabled = existing?.Enabled ?? true,
|
|
};
|
|
|
|
var hooks = _settings.Settings.Llm.AgentHooks;
|
|
if (isNew)
|
|
hooks.Add(entry);
|
|
else if (index >= 0 && index < hooks.Count)
|
|
hooks[index] = entry;
|
|
|
|
BuildOverlayHookCards();
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
dlg.Close();
|
|
};
|
|
btnRow.Children.Add(saveBorder);
|
|
stack.Children.Add(btnRow);
|
|
|
|
border.Child = stack;
|
|
dlg.Content = border;
|
|
dlg.ShowDialog();
|
|
}
|
|
|
|
private static string GetOverlayToolCategory(string toolName)
|
|
{
|
|
return toolName switch
|
|
{
|
|
"file_read" or "file_write" or "file_edit" or "glob" or "grep_tool" or "folder_map" or "document_read" or "file_manage" or "file_info" or "multi_read"
|
|
=> "파일/검색",
|
|
"process" or "build_run" or "dev_env_detect" or "snippet_runner"
|
|
=> "프로세스/빌드",
|
|
"search_codebase" or "code_search" or "code_review" or "lsp" or "test_loop" or "git_tool" or "project_rules" or "project_rule" or "diff_preview"
|
|
=> "코드 분석",
|
|
"excel_create" or "docx_create" or "csv_create" or "markdown_create" or "html_create" or "chart_create" or "batch_create" or "pptx_create" or "document_review" or "format_convert" or "document_planner" or "document_assembler" or "template_render"
|
|
=> "문서 생성",
|
|
"json_tool" or "regex_tool" or "diff_tool" or "base64_tool" or "hash_tool" or "datetime_tool" or "math_tool" or "xml_tool" or "sql_tool" or "data_pivot" or "text_summarize"
|
|
=> "데이터 처리",
|
|
"clipboard_tool" or "notify_tool" or "env_tool" or "zip_tool" or "http_tool" or "open_external" or "image_analyze" or "file_watch"
|
|
=> "시스템/환경",
|
|
"spawn_agent" or "spawn_agents" or "wait_agents" or "memory" or "skill_manager" or "user_ask" or "task_tracker" or "todo_write" or "task_create" or "task_get" or "task_list" or "task_update" or "task_stop" or "task_output" or "enter_plan_mode" or "exit_plan_mode" or "enter_worktree" or "exit_worktree" or "team_create" or "team_delete" or "cron_create" or "cron_delete" or "cron_list" or "suggest_actions" or "checkpoint" or "playbook"
|
|
=> "에이전트",
|
|
_ => "기타"
|
|
};
|
|
}
|
|
|
|
private void CmbOverlayService_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing || CmbOverlayService.SelectedItem is not ComboBoxItem serviceItem || serviceItem.Tag is not string service)
|
|
return;
|
|
|
|
var llm = _settings.Settings.Llm;
|
|
llm.Service = service;
|
|
var candidates = GetModelCandidates(service);
|
|
var preferredModel = service switch
|
|
{
|
|
"ollama" => llm.OllamaModel,
|
|
"vllm" => llm.VllmModel,
|
|
"gemini" => llm.GeminiModel,
|
|
_ => llm.ClaudeModel
|
|
};
|
|
if (!string.IsNullOrWhiteSpace(preferredModel))
|
|
llm.Model = preferredModel;
|
|
else if (candidates.Count > 0 && !candidates.Any(m => m.Id == llm.Model))
|
|
llm.Model = candidates[0].Id;
|
|
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: true);
|
|
}
|
|
|
|
private void CmbOverlayModel_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing || CmbOverlayModel.SelectedItem is not ComboBoxItem modelItem || modelItem.Tag is not string modelId)
|
|
return;
|
|
|
|
CommitOverlayModelSelection(modelId);
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void RefreshOverlayThemeCards()
|
|
{
|
|
var selected = (_settings.Settings.Llm.AgentTheme ?? "system").ToLowerInvariant();
|
|
SetOverlayCardSelection(OverlayThemeSystemCard, selected == "system");
|
|
SetOverlayCardSelection(OverlayThemeLightCard, selected == "light");
|
|
SetOverlayCardSelection(OverlayThemeDarkCard, selected == "dark");
|
|
var preset = (_settings.Settings.Llm.AgentThemePreset ?? "claude").ToLowerInvariant();
|
|
SetOverlayCardSelection(OverlayThemeStyleClaudeCard, preset == "claude");
|
|
SetOverlayCardSelection(OverlayThemeStyleCodexCard, preset == "codex");
|
|
SetOverlayCardSelection(OverlayThemeStyleNordCard, preset == "nord");
|
|
SetOverlayCardSelection(OverlayThemeStyleEmberCard, preset == "ember");
|
|
SetOverlayCardSelection(OverlayThemeStyleSlateCard, preset == "slate");
|
|
}
|
|
|
|
private void RefreshOverlayServiceCards()
|
|
{
|
|
var service = (_settings.Settings.Llm.Service ?? "ollama").ToLowerInvariant();
|
|
SetOverlayCardSelection(OverlaySvcOllamaCard, service == "ollama");
|
|
SetOverlayCardSelection(OverlaySvcVllmCard, service == "vllm");
|
|
SetOverlayCardSelection(OverlaySvcGeminiCard, service == "gemini");
|
|
SetOverlayCardSelection(OverlaySvcClaudeCard, service is "claude" or "sigmoid");
|
|
}
|
|
|
|
private void RefreshOverlayTokenPresetCards()
|
|
{
|
|
var llm = _settings.Settings.Llm;
|
|
var compact = llm.ContextCompactTriggerPercent switch
|
|
{
|
|
<= 60 => 60,
|
|
<= 70 => 70,
|
|
<= 80 => 80,
|
|
_ => 90
|
|
};
|
|
SetOverlayCardSelection(OverlayCompact60Card, compact == 60);
|
|
SetOverlayCardSelection(OverlayCompact70Card, compact == 70);
|
|
SetOverlayCardSelection(OverlayCompact80Card, compact == 80);
|
|
SetOverlayCardSelection(OverlayCompact90Card, compact == 90);
|
|
|
|
var context = llm.MaxContextTokens switch
|
|
{
|
|
<= 4096 => 4096,
|
|
<= 16384 => 16384,
|
|
<= 32768 => 32768,
|
|
<= 65536 => 65536,
|
|
<= 131072 => 131072,
|
|
<= 262144 => 262144,
|
|
_ => 1_000_000
|
|
};
|
|
SetOverlayCardSelection(OverlayContext4KCard, context == 4096);
|
|
SetOverlayCardSelection(OverlayContext16KCard, context == 16384);
|
|
SetOverlayCardSelection(OverlayContext32KCard, context == 32768);
|
|
SetOverlayCardSelection(OverlayContext64KCard, context == 65536);
|
|
SetOverlayCardSelection(OverlayContext128KCard, context == 131072);
|
|
SetOverlayCardSelection(OverlayContext256KCard, context == 262144);
|
|
SetOverlayCardSelection(OverlayContext1MCard, context == 1_000_000);
|
|
}
|
|
|
|
private void RefreshOverlayModeButtons()
|
|
{
|
|
var llm = _settings.Settings.Llm;
|
|
SelectComboTag(CmbOverlayOperationMode, OperationModePolicy.Normalize(_settings.Settings.OperationMode));
|
|
SelectComboTag(CmbOverlayPermission, PermissionModeCatalog.NormalizeGlobalMode(llm.FilePermission));
|
|
SelectComboTag(CmbOverlayReasoning, llm.AgentDecisionLevel);
|
|
SelectComboTag(CmbOverlayFastMode, llm.FreeTierMode ? "on" : "off");
|
|
// CmbOverlayDefaultOutputFormat, CmbOverlayDefaultMood, CmbOverlayAutoPreview 제거됨 (중복 설정 항목)
|
|
SelectComboTag(CmbOverlayAgentLogLevel, llm.AgentLogLevel ?? "detailed");
|
|
UpdateDataUsageUI();
|
|
RefreshOverlayTemperatureModeButtons();
|
|
}
|
|
|
|
private void RefreshOverlayTemperatureModeButtons()
|
|
{
|
|
if (OverlayTemperatureAutoCard == null || OverlayTemperatureCustomCard == null)
|
|
return;
|
|
|
|
var automatic = _settings.Settings.Llm.UseAutomaticProfileTemperature;
|
|
SetOverlayCardSelection(OverlayTemperatureAutoCard, automatic);
|
|
SetOverlayCardSelection(OverlayTemperatureCustomCard, !automatic);
|
|
|
|
if (SldOverlayTemperature != null)
|
|
{
|
|
SldOverlayTemperature.IsEnabled = !automatic;
|
|
SldOverlayTemperature.Opacity = automatic ? 0.55 : 1.0;
|
|
}
|
|
|
|
if (TxtOverlayTemperatureValue != null)
|
|
TxtOverlayTemperatureValue.Opacity = automatic ? 0.65 : 1.0;
|
|
}
|
|
|
|
private static void SelectComboTag(ComboBox? combo, string? tag)
|
|
{
|
|
if (combo == null) return;
|
|
var normalized = (tag ?? "").Trim();
|
|
combo.SelectedItem = combo.Items
|
|
.OfType<ComboBoxItem>()
|
|
.FirstOrDefault(item => string.Equals(item.Tag as string, normalized, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
private void PopulateOverlayMoodCombo()
|
|
{
|
|
// CmbOverlayDefaultMood 제거됨 (중복 설정 항목)
|
|
}
|
|
|
|
private static string GetQuickActionLabel(string title, string value)
|
|
=> $"{title} · {value}";
|
|
|
|
private void RefreshOverlayServiceFieldLabels(string service)
|
|
{
|
|
if (OverlayEndpointLabel == null || OverlayEndpointHint == null || OverlayApiKeyLabel == null || OverlayApiKeyHint == null)
|
|
return;
|
|
|
|
switch (service)
|
|
{
|
|
case "ollama":
|
|
OverlayEndpointLabel.Text = "Ollama 서버 주소";
|
|
OverlayEndpointHint.Text = "사내 로컬 Ollama 기본 주소를 입력합니다.";
|
|
OverlayApiKeyLabel.Text = "Ollama API 키";
|
|
OverlayApiKeyHint.Text = "사내 게이트웨이를 쓰는 경우에만 입력합니다.";
|
|
break;
|
|
case "vllm":
|
|
OverlayEndpointLabel.Text = "vLLM 서버 주소";
|
|
OverlayEndpointHint.Text = "OpenAI 호환 엔드포인트 주소를 입력합니다.";
|
|
OverlayApiKeyLabel.Text = "vLLM API 키";
|
|
OverlayApiKeyHint.Text = "사내 인증 게이트웨이를 쓰는 경우에만 입력합니다.";
|
|
break;
|
|
case "gemini":
|
|
OverlayEndpointLabel.Text = "기본 서버 주소";
|
|
OverlayEndpointHint.Text = "Gemini는 내부 기본 주소를 사용합니다.";
|
|
OverlayApiKeyLabel.Text = "Gemini API 키";
|
|
OverlayApiKeyHint.Text = "외부 호출에 필요한 키를 입력합니다.";
|
|
break;
|
|
default:
|
|
OverlayEndpointLabel.Text = "기본 서버 주소";
|
|
OverlayEndpointHint.Text = "Claude는 내부 기본 주소를 사용합니다.";
|
|
OverlayApiKeyLabel.Text = "Claude API 키";
|
|
OverlayApiKeyHint.Text = "외부 호출에 필요한 키를 입력합니다.";
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void RefreshOverlayServiceFieldVisibility(string service)
|
|
{
|
|
if (OverlayEndpointFieldPanel == null || OverlayApiKeyFieldPanel == null)
|
|
return;
|
|
|
|
var hideEndpoint = string.Equals(service, "gemini", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(service, "claude", StringComparison.OrdinalIgnoreCase);
|
|
|
|
OverlayEndpointFieldPanel.Visibility = hideEndpoint ? Visibility.Collapsed : Visibility.Visible;
|
|
OverlayApiKeyFieldPanel.Margin = hideEndpoint ? new Thickness(0) : new Thickness(6, 0, 0, 0);
|
|
Grid.SetColumn(OverlayApiKeyFieldPanel, hideEndpoint ? 0 : 1);
|
|
Grid.SetColumnSpan(OverlayApiKeyFieldPanel, hideEndpoint ? 2 : 1);
|
|
}
|
|
|
|
private string GetOverlayServiceEndpoint(string service)
|
|
{
|
|
var llm = _settings.Settings.Llm;
|
|
return service switch
|
|
{
|
|
"ollama" => llm.OllamaEndpoint ?? "",
|
|
"vllm" => llm.VllmEndpoint ?? "",
|
|
"gemini" => llm.Endpoint ?? "",
|
|
"claude" or "sigmoid" => llm.Endpoint ?? "",
|
|
_ => llm.Endpoint ?? ""
|
|
};
|
|
}
|
|
|
|
private string GetOverlayServiceApiKey(string service)
|
|
{
|
|
var llm = _settings.Settings.Llm;
|
|
return service switch
|
|
{
|
|
"ollama" => llm.OllamaApiKey ?? "",
|
|
"vllm" => llm.VllmApiKey ?? "",
|
|
"gemini" => llm.GeminiApiKey ?? "",
|
|
"claude" or "sigmoid" => llm.ClaudeApiKey ?? "",
|
|
_ => llm.ApiKey ?? ""
|
|
};
|
|
}
|
|
|
|
private void BuildOverlayRegisteredModelsPanel(string service)
|
|
{
|
|
if (OverlayRegisteredModelsPanel == null || OverlayRegisteredModelsHeader == null || BtnOverlayAddModel == null)
|
|
return;
|
|
|
|
var normalized = NormalizeOverlayService(service);
|
|
var supportsRegistered = SupportsOverlayRegisteredModels(normalized);
|
|
OverlayRegisteredModelsHeader.Visibility = supportsRegistered ? Visibility.Visible : Visibility.Collapsed;
|
|
OverlayRegisteredModelsPanel.Visibility = supportsRegistered ? Visibility.Visible : Visibility.Collapsed;
|
|
BtnOverlayAddModel.Visibility = supportsRegistered ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
OverlayRegisteredModelsPanel.Children.Clear();
|
|
if (!supportsRegistered)
|
|
return;
|
|
|
|
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
|
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
|
var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.White;
|
|
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? BrushFromHex("#F8FAFC");
|
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue;
|
|
|
|
var models = _settings.Settings.Llm.RegisteredModels
|
|
.Where(m => string.Equals(m.Service, normalized, StringComparison.OrdinalIgnoreCase))
|
|
.OrderBy(m => m.Alias, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
if (models.Count == 0)
|
|
{
|
|
OverlayRegisteredModelsPanel.Children.Add(new Border
|
|
{
|
|
CornerRadius = new CornerRadius(10),
|
|
BorderBrush = borderBrush,
|
|
BorderThickness = new Thickness(1),
|
|
Background = Brushes.Transparent,
|
|
Padding = new Thickness(12, 10, 12, 10),
|
|
Child = new TextBlock
|
|
{
|
|
Text = "등록된 모델이 없습니다. `모델 추가`로 사내 모델을 먼저 등록하세요.",
|
|
FontSize = 11.5,
|
|
TextWrapping = TextWrapping.Wrap,
|
|
Foreground = secondaryText,
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
foreach (var model in models)
|
|
{
|
|
var decryptedModelName = Services.CryptoService.DecryptIfEnabled(model.EncryptedModelName, IsOverlayEncryptionEnabled);
|
|
var displayName = string.IsNullOrWhiteSpace(model.Alias) ? decryptedModelName : model.Alias;
|
|
var endpointText = string.IsNullOrWhiteSpace(model.Endpoint) ? "기본 서버 사용" : model.Endpoint;
|
|
var authLabel = (model.AuthType ?? "bearer").ToLowerInvariant() switch
|
|
{
|
|
"cp4d" => "CP4D",
|
|
"ibm_iam" => "IBM IAM",
|
|
_ => "Bearer",
|
|
};
|
|
var isActive = string.Equals(model.EncryptedModelName, _settings.Settings.Llm.Model, StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(decryptedModelName, _settings.Settings.Llm.Model, StringComparison.OrdinalIgnoreCase);
|
|
|
|
var row = new Border
|
|
{
|
|
CornerRadius = new CornerRadius(10),
|
|
BorderBrush = isActive ? accentBrush : borderBrush,
|
|
BorderThickness = new Thickness(1),
|
|
Background = isActive ? (TryFindResource("HintBackground") as Brush ?? BrushFromHex("#EFF6FF")) : itemBg,
|
|
Padding = new Thickness(12, 10, 12, 10),
|
|
Margin = new Thickness(0, 0, 0, 8),
|
|
};
|
|
|
|
var grid = new Grid();
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
|
|
|
var info = new StackPanel();
|
|
info.Children.Add(new TextBlock
|
|
{
|
|
Text = displayName,
|
|
FontSize = 12,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = primaryText,
|
|
});
|
|
info.Children.Add(new TextBlock
|
|
{
|
|
Text = string.IsNullOrWhiteSpace(decryptedModelName) ? "(모델명 없음)" : decryptedModelName,
|
|
Margin = new Thickness(0, 3, 0, 0),
|
|
FontSize = 11,
|
|
Foreground = secondaryText,
|
|
});
|
|
info.Children.Add(new TextBlock
|
|
{
|
|
Text = $"엔드포인트: {endpointText} · 인증: {authLabel}",
|
|
Margin = new Thickness(0, 4, 0, 0),
|
|
FontSize = 10.5,
|
|
TextWrapping = TextWrapping.Wrap,
|
|
Foreground = secondaryText,
|
|
});
|
|
Grid.SetColumn(info, 0);
|
|
grid.Children.Add(info);
|
|
|
|
var actions = new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
VerticalAlignment = VerticalAlignment.Top,
|
|
};
|
|
|
|
Border CreateAction(string text, Action onClick, Brush foreground)
|
|
{
|
|
var label = new TextBlock
|
|
{
|
|
Text = text,
|
|
FontSize = 11.5,
|
|
FontWeight = FontWeights.SemiBold,
|
|
Foreground = foreground,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
};
|
|
|
|
var action = new Border
|
|
{
|
|
Cursor = Cursors.Hand,
|
|
CornerRadius = new CornerRadius(8),
|
|
Padding = new Thickness(8, 4, 8, 4),
|
|
Margin = new Thickness(6, 0, 0, 0),
|
|
Background = Brushes.Transparent,
|
|
Child = label,
|
|
};
|
|
action.MouseEnter += (_, _) =>
|
|
{
|
|
action.Background = hoverBg;
|
|
};
|
|
action.MouseLeave += (_, _) =>
|
|
{
|
|
action.Background = Brushes.Transparent;
|
|
};
|
|
action.MouseLeftButtonUp += (_, _) => onClick();
|
|
return action;
|
|
}
|
|
|
|
actions.Children.Add(CreateAction("선택", () =>
|
|
{
|
|
CommitOverlayModelSelection(model.EncryptedModelName);
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}, accentBrush));
|
|
|
|
actions.Children.Add(CreateAction("편집", () =>
|
|
{
|
|
EditOverlayRegisteredModel(model);
|
|
BuildOverlayRegisteredModelsPanel(service);
|
|
}, primaryText));
|
|
actions.Children.Add(CreateAction("삭제", () =>
|
|
{
|
|
DeleteOverlayRegisteredModel(model);
|
|
}, BrushFromHex("#DC2626")));
|
|
|
|
Grid.SetColumn(actions, 1);
|
|
grid.Children.Add(actions);
|
|
|
|
row.Child = grid;
|
|
row.MouseEnter += (_, _) =>
|
|
{
|
|
if (!isActive)
|
|
row.Background = hoverBg;
|
|
};
|
|
row.MouseLeave += (_, _) =>
|
|
{
|
|
if (!isActive)
|
|
row.Background = itemBg;
|
|
};
|
|
|
|
OverlayRegisteredModelsPanel.Children.Add(row);
|
|
}
|
|
}
|
|
|
|
private void BtnOverlayAddModel_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var service = NormalizeOverlayService(_settings.Settings.Llm.Service);
|
|
if (!SupportsOverlayRegisteredModels(service))
|
|
return;
|
|
|
|
var dlg = new ModelRegistrationDialog(service) { Owner = this };
|
|
if (dlg.ShowDialog() != true)
|
|
return;
|
|
|
|
_settings.Settings.Llm.RegisteredModels.Add(new RegisteredModel
|
|
{
|
|
Alias = dlg.ModelAlias,
|
|
EncryptedModelName = Services.CryptoService.EncryptIfEnabled(dlg.ModelName, IsOverlayEncryptionEnabled),
|
|
Service = service,
|
|
ExecutionProfile = dlg.ExecutionProfile,
|
|
Endpoint = dlg.Endpoint,
|
|
ApiKey = dlg.ApiKey,
|
|
AllowInsecureTls = dlg.AllowInsecureTls,
|
|
AuthType = dlg.AuthType,
|
|
Cp4dUrl = dlg.Cp4dUrl,
|
|
Cp4dUsername = dlg.Cp4dUsername,
|
|
Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsOverlayEncryptionEnabled),
|
|
});
|
|
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: true);
|
|
}
|
|
|
|
private void EditOverlayRegisteredModel(RegisteredModel model)
|
|
{
|
|
var currentModel = Services.CryptoService.DecryptIfEnabled(model.EncryptedModelName, IsOverlayEncryptionEnabled);
|
|
var cp4dPassword = Services.CryptoService.DecryptIfEnabled(model.Cp4dPassword ?? "", IsOverlayEncryptionEnabled);
|
|
var service = NormalizeOverlayService(model.Service);
|
|
|
|
var dlg = new ModelRegistrationDialog(
|
|
service,
|
|
model.Alias,
|
|
currentModel,
|
|
model.Endpoint,
|
|
model.ApiKey,
|
|
model.AllowInsecureTls,
|
|
model.AuthType ?? "bearer",
|
|
model.Cp4dUrl ?? "",
|
|
model.Cp4dUsername ?? "",
|
|
cp4dPassword,
|
|
model.ExecutionProfile ?? "balanced")
|
|
{ Owner = this };
|
|
|
|
if (dlg.ShowDialog() != true)
|
|
return;
|
|
|
|
model.Alias = dlg.ModelAlias;
|
|
model.EncryptedModelName = Services.CryptoService.EncryptIfEnabled(dlg.ModelName, IsOverlayEncryptionEnabled);
|
|
model.Service = service;
|
|
model.ExecutionProfile = dlg.ExecutionProfile;
|
|
model.Endpoint = dlg.Endpoint;
|
|
model.ApiKey = dlg.ApiKey;
|
|
model.AllowInsecureTls = dlg.AllowInsecureTls;
|
|
model.AuthType = dlg.AuthType;
|
|
model.Cp4dUrl = dlg.Cp4dUrl;
|
|
model.Cp4dUsername = dlg.Cp4dUsername;
|
|
model.Cp4dPassword = Services.CryptoService.EncryptIfEnabled(dlg.Cp4dPassword, IsOverlayEncryptionEnabled);
|
|
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: true);
|
|
}
|
|
|
|
private void DeleteOverlayRegisteredModel(RegisteredModel model)
|
|
{
|
|
var result = CustomMessageBox.Show($"'{model.Alias}' 모델을 삭제하시겠습니까?", "모델 삭제",
|
|
MessageBoxButton.YesNo, MessageBoxImage.Question);
|
|
if (result != MessageBoxResult.Yes)
|
|
return;
|
|
|
|
_settings.Settings.Llm.RegisteredModels.Remove(model);
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: true);
|
|
}
|
|
|
|
private void SetOverlayCardSelection(Border border, bool selected)
|
|
{
|
|
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue;
|
|
var normal = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
|
border.BorderBrush = selected ? accent : normal;
|
|
border.Background = selected
|
|
? (TryFindResource("HintBackground") as Brush ?? Brushes.Transparent)
|
|
: Brushes.Transparent;
|
|
}
|
|
|
|
private void OverlayThemeSystemCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
_settings.Settings.Llm.AgentTheme = "system";
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void OverlayThemeLightCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
_settings.Settings.Llm.AgentTheme = "light";
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void OverlayThemeDarkCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
_settings.Settings.Llm.AgentTheme = "dark";
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void OverlayThemeStyleClaudeCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
_settings.Settings.Llm.AgentThemePreset = "claude";
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void OverlayThemeStyleCodexCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
_settings.Settings.Llm.AgentThemePreset = "codex";
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void OverlayThemeStyleNordCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
_settings.Settings.Llm.AgentThemePreset = "nord";
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void OverlayThemeStyleEmberCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
_settings.Settings.Llm.AgentThemePreset = "ember";
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void OverlayThemeStyleSlateCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
{
|
|
_settings.Settings.Llm.AgentThemePreset = "slate";
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void OverlaySvcOllamaCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetOverlayService("ollama");
|
|
private void OverlaySvcVllmCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetOverlayService("vllm");
|
|
private void OverlaySvcGeminiCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetOverlayService("gemini");
|
|
private void OverlaySvcClaudeCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetOverlayService("claude");
|
|
|
|
private void SetOverlayService(string service)
|
|
{
|
|
_settings.Settings.Llm.Service = service;
|
|
var llm = _settings.Settings.Llm;
|
|
var candidates = GetModelCandidates(service);
|
|
var preferredModel = service switch
|
|
{
|
|
"ollama" => llm.OllamaModel,
|
|
"vllm" => llm.VllmModel,
|
|
"gemini" => llm.GeminiModel,
|
|
_ => llm.ClaudeModel
|
|
};
|
|
llm.Model = !string.IsNullOrWhiteSpace(preferredModel)
|
|
? preferredModel
|
|
: candidates.FirstOrDefault().Id ?? llm.Model;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: true);
|
|
RefreshOverlayVisualState(loadDeferredInputs: true);
|
|
}
|
|
|
|
private void BtnOverlayOperationMode_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var next = OperationModePolicy.Normalize(_settings.Settings.OperationMode) == OperationModePolicy.ExternalMode
|
|
? OperationModePolicy.InternalMode
|
|
: OperationModePolicy.ExternalMode;
|
|
|
|
if (!PromptOverlayPasswordDialog("운영 모드 변경", "사내/사외 모드 변경", "비밀번호를 입력하세요."))
|
|
return;
|
|
|
|
_settings.Settings.OperationMode = next;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void BtnOverlayFolderDataUsage_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
_folderDataUsage = GetAutomaticFolderDataUsage();
|
|
}
|
|
|
|
private void CmbOverlayFastMode_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing || CmbOverlayFastMode.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag)
|
|
return;
|
|
|
|
_settings.Settings.Llm.FreeTierMode = string.Equals(tag, "on", StringComparison.OrdinalIgnoreCase);
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void CmbOverlayReasoning_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing || CmbOverlayReasoning.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag)
|
|
return;
|
|
|
|
_settings.Settings.Llm.AgentDecisionLevel = tag;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void CmbOverlayPermission_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing || CmbOverlayPermission.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag)
|
|
return;
|
|
|
|
var normalized = PermissionModeCatalog.NormalizeGlobalMode(tag);
|
|
var llm = _settings.Settings.Llm;
|
|
llm.FilePermission = normalized;
|
|
llm.DefaultAgentPermission = normalized;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void CmbOverlayDefaultOutputFormat_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
// 제거됨 (중복 설정 항목)
|
|
}
|
|
|
|
private void CmbOverlayDefaultMood_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
// 제거됨 (중복 설정 항목)
|
|
}
|
|
|
|
private void CmbOverlayAutoPreview_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing) return;
|
|
ApplyOverlaySettingsChanges(showToast: false, closeOverlay: false);
|
|
}
|
|
|
|
private void CmbOverlayOperationMode_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing || CmbOverlayOperationMode.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag)
|
|
return;
|
|
|
|
var normalized = OperationModePolicy.Normalize(tag);
|
|
var current = OperationModePolicy.Normalize(_settings.Settings.OperationMode);
|
|
if (string.Equals(normalized, current, StringComparison.OrdinalIgnoreCase))
|
|
return;
|
|
|
|
if (string.Equals(normalized, OperationModePolicy.ExternalMode, StringComparison.OrdinalIgnoreCase) &&
|
|
!PromptOverlayPasswordDialog("운영 모드 변경", "사내/사외 모드 변경", "비밀번호를 입력하세요."))
|
|
{
|
|
RefreshOverlayModeButtons();
|
|
return;
|
|
}
|
|
|
|
_settings.Settings.OperationMode = normalized;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void CmbOverlayFolderDataUsage_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
_folderDataUsage = GetAutomaticFolderDataUsage();
|
|
}
|
|
|
|
private void CmbOverlayAgentLogLevel_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (_isOverlaySettingsSyncing || CmbOverlayAgentLogLevel.SelectedItem is not ComboBoxItem selected || selected.Tag is not string tag)
|
|
return;
|
|
|
|
_settings.Settings.Llm.AgentLogLevel = tag;
|
|
PersistOverlaySettingsState(refreshOverlayDeferredInputs: false);
|
|
}
|
|
|
|
private void BtnOpenFullSettings_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (System.Windows.Application.Current is App app)
|
|
app.OpenSettingsFromChat();
|
|
}
|
|
|
|
private bool PromptOverlayPasswordDialog(string title, string header, string message)
|
|
{
|
|
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? Brushes.White;
|
|
var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
|
var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
|
var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.WhiteSmoke;
|
|
|
|
var dlg = new Window
|
|
{
|
|
Title = title,
|
|
Width = 340,
|
|
SizeToContent = SizeToContent.Height,
|
|
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
|
Owner = this,
|
|
ResizeMode = ResizeMode.NoResize,
|
|
WindowStyle = WindowStyle.None,
|
|
AllowsTransparency = true,
|
|
Background = Brushes.Transparent,
|
|
ShowInTaskbar = false,
|
|
};
|
|
|
|
var border = new Border
|
|
{
|
|
Background = bgBrush,
|
|
CornerRadius = new CornerRadius(12),
|
|
BorderBrush = borderBrush,
|
|
BorderThickness = new Thickness(1),
|
|
Padding = new Thickness(20),
|
|
};
|
|
|
|
var stack = new StackPanel();
|
|
stack.Children.Add(new TextBlock { Text = header, FontSize = 15, FontWeight = FontWeights.SemiBold, Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 12) });
|
|
stack.Children.Add(new TextBlock { Text = message, FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 6) });
|
|
|
|
var pwBox = new PasswordBox
|
|
{
|
|
FontSize = 14,
|
|
Padding = new Thickness(8, 6, 8, 6),
|
|
Background = itemBg,
|
|
Foreground = fgBrush,
|
|
BorderBrush = borderBrush,
|
|
PasswordChar = '*',
|
|
};
|
|
stack.Children.Add(pwBox);
|
|
|
|
var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) };
|
|
var cancelBtn = new Button { Content = "취소", Padding = new Thickness(16, 6, 16, 6), Margin = new Thickness(0, 0, 8, 0) };
|
|
cancelBtn.Click += (_, _) => dlg.DialogResult = false;
|
|
btnRow.Children.Add(cancelBtn);
|
|
|
|
var okBtn = new Button { Content = "확인", Padding = new Thickness(16, 6, 16, 6), IsDefault = true };
|
|
okBtn.Click += (_, _) =>
|
|
{
|
|
if (pwBox.Password == UnifiedAdminPassword)
|
|
dlg.DialogResult = true;
|
|
else
|
|
{
|
|
pwBox.Clear();
|
|
pwBox.Focus();
|
|
}
|
|
};
|
|
btnRow.Children.Add(okBtn);
|
|
stack.Children.Add(btnRow);
|
|
|
|
border.Child = stack;
|
|
dlg.Content = border;
|
|
dlg.Loaded += (_, _) => pwBox.Focus();
|
|
return dlg.ShowDialog() == true;
|
|
}
|
|
|
|
private static int ParseOverlayInt(string? text, int fallback, int min, int max)
|
|
{
|
|
if (!int.TryParse(text, out var value))
|
|
value = fallback;
|
|
return Math.Clamp(value, min, max);
|
|
}
|
|
|
|
private void BtnInlineSettingsClose_Click(object sender, RoutedEventArgs e)
|
|
=> InlineSettingsPanel.IsOpen = false;
|
|
|
|
private void CmbInlineService_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (_isInlineSettingsSyncing || CmbInlineService.SelectedItem is not ComboBoxItem serviceItem || serviceItem.Tag is not string service)
|
|
return;
|
|
|
|
var llm = _settings.Settings.Llm;
|
|
llm.Service = service;
|
|
|
|
var candidates = GetModelCandidates(service);
|
|
if (candidates.Count > 0 && !candidates.Any(m => m.Id == llm.Model))
|
|
llm.Model = candidates[0].Id;
|
|
|
|
ScheduleSettingsSave();
|
|
_appState.LoadFromSettings(_settings);
|
|
UpdateModelLabel();
|
|
RefreshInlineSettingsPanel();
|
|
}
|
|
|
|
private void CmbInlineModel_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (_isInlineSettingsSyncing || CmbInlineModel.SelectedItem is not ComboBoxItem modelItem || modelItem.Tag is not string modelId)
|
|
return;
|
|
|
|
_settings.Settings.Llm.Model = modelId;
|
|
ScheduleSettingsSave();
|
|
_appState.LoadFromSettings(_settings);
|
|
UpdateModelLabel();
|
|
RefreshInlineSettingsPanel();
|
|
}
|
|
|
|
private void BtnInlineFastMode_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
_settings.Settings.Llm.FreeTierMode = !_settings.Settings.Llm.FreeTierMode;
|
|
ScheduleSettingsSave();
|
|
_appState.LoadFromSettings(_settings);
|
|
RefreshInlineSettingsPanel();
|
|
RefreshOverlayVisualState(loadDeferredInputs: false);
|
|
}
|
|
|
|
private void BtnInlineReasoning_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var llm = _settings.Settings.Llm;
|
|
llm.AgentDecisionLevel = NextReasoning(llm.AgentDecisionLevel);
|
|
ScheduleSettingsSave();
|
|
_appState.LoadFromSettings(_settings);
|
|
RefreshInlineSettingsPanel();
|
|
RefreshOverlayVisualState(loadDeferredInputs: false);
|
|
}
|
|
|
|
private void BtnInlinePermission_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var llm = _settings.Settings.Llm;
|
|
llm.FilePermission = NextPermission(llm.FilePermission);
|
|
ScheduleSettingsSave();
|
|
_appState.LoadFromSettings(_settings);
|
|
UpdatePermissionUI();
|
|
SaveConversationSettings();
|
|
RefreshInlineSettingsPanel();
|
|
RefreshOverlayVisualState(loadDeferredInputs: false);
|
|
}
|
|
|
|
private void BtnInlineSkill_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var llm = _settings.Settings.Llm;
|
|
llm.EnableSkillSystem = !llm.EnableSkillSystem;
|
|
if (llm.EnableSkillSystem)
|
|
{
|
|
SkillService.EnsureSkillFolder();
|
|
SkillService.LoadSkills(llm.SkillsFolderPath, GetCurrentWorkFolder(), llm.AdditionalSkillFolders);
|
|
UpdateConditionalSkillActivation(reset: true);
|
|
}
|
|
|
|
ScheduleSettingsSave();
|
|
_appState.LoadFromSettings(_settings);
|
|
RefreshInlineSettingsPanel();
|
|
|
|
if (llm.EnableSkillSystem)
|
|
OpenCommandSkillBrowser("/");
|
|
}
|
|
|
|
private void BtnInlineCommandBrowser_Click(object sender, RoutedEventArgs e)
|
|
=> OpenCommandSkillBrowser("/");
|
|
|
|
private void BtnInlineMcp_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var app = System.Windows.Application.Current as App;
|
|
app?.OpenSettingsFromChat();
|
|
}
|
|
|
|
private void BtnNewChat_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
StartNewConversation();
|
|
InputBox.Focus();
|
|
}
|
|
|
|
public void ResumeConversation(string conversationId)
|
|
{
|
|
var conv = _storage.Load(conversationId);
|
|
if (conv != null)
|
|
{
|
|
var targetTab = NormalizeTabName(conv.Tab);
|
|
if (!string.Equals(_activeTab, targetTab, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
SaveCurrentTabConversationId();
|
|
PersistPerTabUiState();
|
|
_activeTab = targetTab;
|
|
RestorePerTabUiState();
|
|
UpdateTabUI();
|
|
|
|
if (string.Equals(targetTab, "Chat", StringComparison.OrdinalIgnoreCase))
|
|
TabChat.IsChecked = true;
|
|
else if (string.Equals(targetTab, "Cowork", StringComparison.OrdinalIgnoreCase))
|
|
TabCowork.IsChecked = true;
|
|
else if (TabCode.IsEnabled)
|
|
TabCode.IsChecked = true;
|
|
}
|
|
|
|
lock (_convLock)
|
|
{
|
|
conv.Tab = targetTab;
|
|
_currentConversation = ChatSession?.SetCurrentConversation(targetTab, conv, _storage) ?? conv;
|
|
SyncTabConversationIdsFromSession();
|
|
}
|
|
SaveLastConversations();
|
|
UpdateChatTitle();
|
|
RefreshConversationList();
|
|
RenderMessages();
|
|
UpdateFolderBar();
|
|
RefreshDraftQueueUi();
|
|
}
|
|
InputBox.Focus();
|
|
}
|
|
|
|
private static string NormalizeTabName(string? tab)
|
|
{
|
|
var normalized = (tab ?? "").Trim();
|
|
if (string.IsNullOrEmpty(normalized))
|
|
return "Chat";
|
|
|
|
if (normalized.Contains("코워크", StringComparison.OrdinalIgnoreCase))
|
|
return "Cowork";
|
|
|
|
var canonical = new string(normalized
|
|
.Where(char.IsLetterOrDigit)
|
|
.ToArray())
|
|
.ToLowerInvariant();
|
|
|
|
if (canonical is "cowork" or "coworkcode" or "coworkcodetab")
|
|
return "Cowork";
|
|
|
|
if (normalized.Contains("코드", StringComparison.OrdinalIgnoreCase)
|
|
|| canonical is "code" or "codetab")
|
|
return "Code";
|
|
|
|
return "Chat";
|
|
}
|
|
|
|
public void StartNewAndFocus()
|
|
{
|
|
StartNewConversation();
|
|
InputBox.Focus();
|
|
}
|
|
|
|
private void BtnDeleteAll_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var tabLabel = _activeTab switch
|
|
{
|
|
"Cowork" => "코워크",
|
|
"Code" => "코드",
|
|
_ => "채팅"
|
|
};
|
|
var result = CustomMessageBox.Show(
|
|
$"'{tabLabel}' 탭의 모든 대화 내역을 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.",
|
|
$"{tabLabel} 대화 전체 삭제",
|
|
MessageBoxButton.YesNo,
|
|
MessageBoxImage.Warning);
|
|
|
|
if (result != MessageBoxResult.Yes) return;
|
|
|
|
_storage.DeleteAllByTab(_activeTab);
|
|
lock (_convLock)
|
|
{
|
|
ChatSession?.ClearCurrentConversation(_activeTab);
|
|
_currentConversation = null;
|
|
SyncTabConversationIdsFromSession();
|
|
}
|
|
ClearTranscriptElements();
|
|
EmptyState.Visibility = Visibility.Visible;
|
|
UpdateChatTitle();
|
|
RefreshConversationList();
|
|
}
|
|
}
|