변경 목적: - AX Agent의 도구 이름, 내부 설정, 스킬 정책, 실행 루프 사이의 불일치를 줄이고 전체 동작 품질을 높인다. - claw-code 수준의 일관된 동작 품질을 참고하되 AX 구조에 맞는 고유한 카탈로그·정규화 레이어로 재구성한다. 핵심 수정사항: - 도구 canonical id, legacy alias, 탭 노출, 설정 카테고리, read-only 분류를 중앙 카탈로그로 통합했다. - ToolRegistry, AgentLoopService, 병렬 실행 분류, 권한 처리, 훅 처리, 스킬 allowed-tools 해석이 같은 이름 체계를 사용하도록 정리했다. - Agent 설정/일반 설정/도움말의 도구 카드와 훅 편집기, 스킬 설명을 현재 런타임 구조에 맞게 갱신했다. - 컨텍스트 압축, intent gate, spawn agents, session learning, model prompt adapter, workspace context 관련 변경과 테스트 추가를 함께 반영했다. - 문서 이력과 비교/로드맵 문서를 최신 상태로 갱신했다. 검증 결과: - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_toolcat\ -p:IntermediateOutputPath=obj\verify_toolcat\ : 경고 0 / 오류 0 - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentToolCatalogTests -p:OutputPath=bin\verify_toolcat_tests\ -p:IntermediateOutputPath=obj\verify_toolcat_tests\ : 통과 8
3779 lines
163 KiB
C#
3779 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);
|
|
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 = skills
|
|
.Where(skill => skill.IsAvailable && (!skill.UserInvocable || !string.IsNullOrWhiteSpace(skill.Paths)))
|
|
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
var directSkills = skills
|
|
.Where(skill => skill.IsAvailable && skill.UserInvocable && string.IsNullOrWhiteSpace(skill.Paths))
|
|
.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);
|
|
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();
|
|
}
|
|
}
|