재구성 AX Agent 설정과 채팅 UI를 Claude형 구조로
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
This commit is contained in:
@@ -15,6 +15,7 @@ namespace AxCopilot.Views;
|
||||
/// <summary>AX Agent 창. 데스크톱 코파일럿 스타일 — 사이드바 + 카테고리 분류 + 타임라인.</summary>
|
||||
public partial class ChatWindow : Window
|
||||
{
|
||||
private const string UnifiedAdminPassword = "axgo123!";
|
||||
private readonly SettingsService _settings;
|
||||
private readonly ChatStorageService _storage;
|
||||
private readonly DraftQueueProcessorService _draftQueueProcessor = new();
|
||||
@@ -76,7 +77,7 @@ public partial class ChatWindow : Window
|
||||
private readonly DispatcherTimer _typingTimer;
|
||||
private int _displayedLength; // 현재 화면에 표시된 글자 수
|
||||
private ResourceDictionary? _agentThemeDictionary;
|
||||
private AgentSettingsWindow? _agentSettingsWindow;
|
||||
private bool _isOverlaySettingsSyncing;
|
||||
|
||||
private sealed class ConversationMeta
|
||||
{
|
||||
@@ -984,6 +985,15 @@ public partial class ChatWindow : Window
|
||||
ApplyAgentThemeResources();
|
||||
ApplyExpressionLevelUi();
|
||||
ApplySidebarStateForActiveTab(animated: false);
|
||||
if (CurrentTabTitle != null)
|
||||
{
|
||||
CurrentTabTitle.Text = _activeTab switch
|
||||
{
|
||||
"Cowork" => "AX Agent · Cowork",
|
||||
"Code" => "AX Agent · Code",
|
||||
_ => "AX Agent · Chat",
|
||||
};
|
||||
}
|
||||
|
||||
// 폴더 바는 Cowork/Code 탭에서만 표시
|
||||
if (FolderBar != null)
|
||||
@@ -1892,9 +1902,9 @@ public partial class ChatWindow : Window
|
||||
return button;
|
||||
}
|
||||
|
||||
actionRow.Children.Add(CreateActionButton("활용하지 않음", "#FEF2F2", "#991B1B", () => SetToolPermissionOverride(latestDenied.ToolName!, PermissionModeCatalog.Deny)));
|
||||
actionRow.Children.Add(CreateActionButton("소극 활용", "#EEF2FF", "#1D4ED8", () => SetToolPermissionOverride(latestDenied.ToolName!, PermissionModeCatalog.Default)));
|
||||
actionRow.Children.Add(CreateActionButton("적극 활용", "#ECFDF5", "#166534", () => SetToolPermissionOverride(latestDenied.ToolName!, PermissionModeCatalog.AcceptEdits)));
|
||||
actionRow.Children.Add(CreateActionButton("읽기 전용", "#FEF2F2", "#991B1B", () => SetToolPermissionOverride(latestDenied.ToolName!, PermissionModeCatalog.Deny)));
|
||||
actionRow.Children.Add(CreateActionButton("권한 요청", "#EEF2FF", "#1D4ED8", () => SetToolPermissionOverride(latestDenied.ToolName!, PermissionModeCatalog.Default)));
|
||||
actionRow.Children.Add(CreateActionButton("편집 자동 승인", "#ECFDF5", "#166534", () => SetToolPermissionOverride(latestDenied.ToolName!, PermissionModeCatalog.AcceptEdits)));
|
||||
actionRow.Children.Add(CreateActionButton("예외 해제", "#F3F4F6", "#374151", () => SetToolPermissionOverride(latestDenied.ToolName!, null)));
|
||||
deniedStack.Children.Add(actionRow);
|
||||
}
|
||||
@@ -2117,7 +2127,6 @@ public partial class ChatWindow : Window
|
||||
BtnPermission_Click(this, new RoutedEventArgs());
|
||||
}
|
||||
|
||||
private bool _permissionTopBannerDismissed; // 상단 권한 배너 닫힘 상태
|
||||
private string _lastPermissionBannerMode = "";
|
||||
|
||||
private bool GetPermissionPopupSectionExpanded(string sectionKey, bool defaultValue = false)
|
||||
@@ -2137,7 +2146,6 @@ public partial class ChatWindow : Window
|
||||
|
||||
private void BtnPermissionTopBannerClose_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_permissionTopBannerDismissed = true;
|
||||
if (PermissionTopBanner != null)
|
||||
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
@@ -2167,7 +2175,6 @@ public partial class ChatWindow : Window
|
||||
|
||||
if (!string.Equals(_lastPermissionBannerMode, perm, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_permissionTopBannerDismissed = false;
|
||||
_lastPermissionBannerMode = perm;
|
||||
}
|
||||
|
||||
@@ -2182,10 +2189,10 @@ public partial class ChatWindow : Window
|
||||
PermissionTopBanner.BorderBrush = BrushFromHex("#86EFAC");
|
||||
PermissionTopBannerIcon.Text = "\uE73E";
|
||||
PermissionTopBannerIcon.Foreground = activeColor;
|
||||
PermissionTopBannerTitle.Text = "현재 권한 모드 · 적극 활용";
|
||||
PermissionTopBannerTitle.Text = "현재 권한 모드 · 편집 자동 승인";
|
||||
PermissionTopBannerTitle.Foreground = BrushFromHex("#166534");
|
||||
PermissionTopBannerText.Text = "파일 편집 도구는 자동 승인하고, 명령 실행은 계속 확인합니다.";
|
||||
PermissionTopBanner.Visibility = _permissionTopBannerDismissed ? Visibility.Collapsed : Visibility.Visible;
|
||||
PermissionTopBannerText.Text = "모든 파일 편집을 자동 승인합니다. 명령 실행은 계속 확인합니다.";
|
||||
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
else if (perm == PermissionModeCatalog.Deny)
|
||||
@@ -2198,10 +2205,10 @@ public partial class ChatWindow : Window
|
||||
PermissionTopBanner.BorderBrush = BrushFromHex("#86EFAC");
|
||||
PermissionTopBannerIcon.Text = "\uE73E";
|
||||
PermissionTopBannerIcon.Foreground = denyColor;
|
||||
PermissionTopBannerTitle.Text = "현재 권한 모드 · 활용하지 않음";
|
||||
PermissionTopBannerTitle.Text = "현재 권한 모드 · 읽기 전용";
|
||||
PermissionTopBannerTitle.Foreground = denyColor;
|
||||
PermissionTopBannerText.Text = "파일 읽기만 허용하고 생성/수정/삭제는 차단합니다.";
|
||||
PermissionTopBanner.Visibility = _permissionTopBannerDismissed ? Visibility.Collapsed : Visibility.Visible;
|
||||
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
else if (perm == PermissionModeCatalog.BypassPermissions)
|
||||
@@ -2214,10 +2221,10 @@ public partial class ChatWindow : Window
|
||||
PermissionTopBanner.BorderBrush = BrushFromHex("#FDBA74");
|
||||
PermissionTopBannerIcon.Text = "\uE814";
|
||||
PermissionTopBannerIcon.Foreground = autoColor;
|
||||
PermissionTopBannerTitle.Text = "현재 권한 모드 · 완전 자동";
|
||||
PermissionTopBannerTitle.Text = "현재 권한 모드 · 권한 건너뛰기";
|
||||
PermissionTopBannerTitle.Foreground = autoColor;
|
||||
PermissionTopBannerText.Text = "권한 확인을 대부분 생략합니다. 민감한 작업 전에는 설정을 다시 확인하세요.";
|
||||
PermissionTopBanner.Visibility = _permissionTopBannerDismissed ? Visibility.Collapsed : Visibility.Visible;
|
||||
PermissionTopBannerText.Text = "모든 권한을 허용합니다. 민감한 작업 전에는 설정을 다시 확인하세요.";
|
||||
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
else if (perm == PermissionModeCatalog.DontAsk)
|
||||
@@ -2233,7 +2240,7 @@ public partial class ChatWindow : Window
|
||||
PermissionTopBannerTitle.Text = "현재 권한 모드 · 질문 없이 진행";
|
||||
PermissionTopBannerTitle.Foreground = dangerColor;
|
||||
PermissionTopBannerText.Text = "권한 확인을 거의 생략합니다. 민감한 작업 전에는 설정을 다시 확인하세요.";
|
||||
PermissionTopBanner.Visibility = _permissionTopBannerDismissed ? Visibility.Collapsed : Visibility.Visible;
|
||||
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -2253,20 +2260,20 @@ public partial class ChatWindow : Window
|
||||
PermissionTopBanner.BorderBrush = BrushFromHex("#C7D2FE");
|
||||
PermissionTopBannerIcon.Text = "\uE7C3";
|
||||
PermissionTopBannerIcon.Foreground = BrushFromHex("#4338CA");
|
||||
PermissionTopBannerTitle.Text = "현재 권한 모드 · 계획 중심";
|
||||
PermissionTopBannerTitle.Text = "현재 권한 모드 · 계획 모드";
|
||||
PermissionTopBannerTitle.Foreground = BrushFromHex("#4338CA");
|
||||
PermissionTopBannerText.Text = "쓰기 작업은 제한하고, 먼저 계획과 승인 흐름을 우선합니다.";
|
||||
PermissionTopBanner.Visibility = _permissionTopBannerDismissed ? Visibility.Collapsed : Visibility.Visible;
|
||||
PermissionTopBannerText.Text = "변경 전에 계획을 먼저 만들고 승인 흐름을 우선합니다.";
|
||||
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
else if (perm == PermissionModeCatalog.Default)
|
||||
{
|
||||
PermissionTopBanner.BorderBrush = BrushFromHex("#BFDBFE");
|
||||
PermissionTopBannerIcon.Text = "\uE8D7";
|
||||
PermissionTopBannerIcon.Foreground = BrushFromHex("#1D4ED8");
|
||||
PermissionTopBannerTitle.Text = "현재 권한 모드 · 소극 활용";
|
||||
PermissionTopBannerTitle.Text = "현재 권한 모드 · 권한 요청";
|
||||
PermissionTopBannerTitle.Foreground = BrushFromHex("#1D4ED8");
|
||||
PermissionTopBannerText.Text = "변경 전 확인하고, 필요한 경우에만 파일 접근을 진행합니다.";
|
||||
PermissionTopBanner.Visibility = _permissionTopBannerDismissed ? Visibility.Collapsed : Visibility.Visible;
|
||||
PermissionTopBannerText.Text = "변경하기 전에 항상 확인합니다.";
|
||||
PermissionTopBanner.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -5667,8 +5674,20 @@ public partial class ChatWindow : Window
|
||||
if (!_slashVisibleItemByAbsoluteIndex.TryGetValue(_slashPalette.SelectedIndex, out var item))
|
||||
return;
|
||||
|
||||
var bounds = item.TransformToAncestor(SlashScrollViewer)
|
||||
.TransformBounds(new Rect(0, 0, item.ActualWidth, item.ActualHeight));
|
||||
if (!IsVisualDescendantOf(item, SlashScrollViewer))
|
||||
return;
|
||||
|
||||
Rect bounds;
|
||||
try
|
||||
{
|
||||
bounds = item.TransformToAncestor(SlashScrollViewer)
|
||||
.TransformBounds(new Rect(0, 0, item.ActualWidth, item.ActualHeight));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 렌더 트리 갱신 중에는 transform이 실패할 수 있어 조용히 무시.
|
||||
return;
|
||||
}
|
||||
|
||||
if (bounds.Top < 0)
|
||||
SlashScrollViewer.ScrollToVerticalOffset(SlashScrollViewer.VerticalOffset + bounds.Top - 8);
|
||||
@@ -5676,6 +5695,22 @@ public partial class ChatWindow : Window
|
||||
SlashScrollViewer.ScrollToVerticalOffset(SlashScrollViewer.VerticalOffset + (bounds.Bottom - SlashScrollViewer.ViewportHeight) + 8);
|
||||
}
|
||||
|
||||
private static bool IsVisualDescendantOf(DependencyObject? child, DependencyObject? parent)
|
||||
{
|
||||
if (child == null || parent == null)
|
||||
return false;
|
||||
|
||||
var current = child;
|
||||
while (current != null)
|
||||
{
|
||||
if (ReferenceEquals(current, parent))
|
||||
return true;
|
||||
current = VisualTreeHelper.GetParent(current);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void UpdateSlashSelectionVisualState()
|
||||
{
|
||||
if (_slashVisibleItemByAbsoluteIndex.Count == 0)
|
||||
@@ -12997,66 +13032,17 @@ public partial class ChatWindow : Window
|
||||
|
||||
private void OpenAgentSettingsWindow()
|
||||
{
|
||||
if (_agentSettingsWindow != null)
|
||||
RefreshOverlaySettingsPanel();
|
||||
AgentSettingsOverlay.Visibility = Visibility.Visible;
|
||||
InlineSettingsPanel.Visibility = Visibility.Collapsed;
|
||||
SetOverlaySection("common");
|
||||
Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_agentSettingsWindow.IsVisible)
|
||||
{
|
||||
_agentSettingsWindow.Activate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore stale window instance
|
||||
}
|
||||
_agentSettingsWindow = null;
|
||||
}
|
||||
|
||||
var win = new AgentSettingsWindow(_settings);
|
||||
if (IsLoaded && IsVisible)
|
||||
win.Owner = this;
|
||||
_agentSettingsWindow = win;
|
||||
win.Closed += (_, _) => _agentSettingsWindow = null;
|
||||
try
|
||||
{
|
||||
win.Resources.MergedDictionaries.Insert(0, new ResourceDictionary
|
||||
{
|
||||
Source = BuildAgentThemeDictionaryUri(),
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 테마 사전 로드 실패 시에도 설정창 자체는 열리도록 유지
|
||||
}
|
||||
|
||||
bool changed;
|
||||
try
|
||||
{
|
||||
changed = win.ShowDialog() == true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 모달 창 오픈에 실패하면 일반 창으로라도 설정 접근을 보장
|
||||
try
|
||||
{
|
||||
win.Show();
|
||||
win.Activate();
|
||||
}
|
||||
catch { }
|
||||
return;
|
||||
}
|
||||
if (!changed)
|
||||
return;
|
||||
|
||||
_appState.LoadFromSettings(_settings);
|
||||
ApplyAgentThemeResources();
|
||||
UpdateSidebarModeMenu();
|
||||
UpdateModelLabel();
|
||||
RefreshInlineSettingsPanel();
|
||||
UpdateTabUI();
|
||||
ShowToast("AX Agent 설정이 저장되었습니다.");
|
||||
if (CmbOverlayModel.Items.Count > 0)
|
||||
CmbOverlayModel.Focus();
|
||||
else
|
||||
InputBox.Focus();
|
||||
}, DispatcherPriority.Input);
|
||||
}
|
||||
|
||||
public void OpenAgentSettingsFromExternal()
|
||||
@@ -13069,6 +13055,515 @@ public partial class ChatWindow : Window
|
||||
}, DispatcherPriority.Input);
|
||||
}
|
||||
|
||||
private void BtnOverlaySettingsClose_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
AgentSettingsOverlay.Visibility = Visibility.Collapsed;
|
||||
InputBox.Focus();
|
||||
}
|
||||
|
||||
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) ? "common" : tag.Trim().ToLowerInvariant();
|
||||
|
||||
OverlaySectionService.Visibility = section == "service" ? Visibility.Visible : Visibility.Collapsed;
|
||||
OverlaySectionQuick.Visibility = section == "permission" ? Visibility.Visible : Visibility.Collapsed;
|
||||
OverlaySectionDetail.Visibility = section == "service" ? Visibility.Collapsed : Visibility.Visible;
|
||||
|
||||
var showCommon = section == "common";
|
||||
var showPermission = section == "permission";
|
||||
var showAdvanced = section == "advanced";
|
||||
|
||||
if (OverlayAiEnabledRow != null)
|
||||
OverlayAiEnabledRow.Visibility = showCommon ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (OverlayThemePanel != null)
|
||||
OverlayThemePanel.Visibility = showCommon ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (OverlayModelEditorPanel != null)
|
||||
OverlayModelEditorPanel.Visibility = showCommon ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (OverlayAnchorPermission != null)
|
||||
OverlayAnchorPermission.Visibility = showPermission ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (OverlayFolderDataUsageRow != null)
|
||||
OverlayFolderDataUsageRow.Visibility = showCommon || showPermission ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (OverlayTlsRow != null)
|
||||
OverlayTlsRow.Visibility = showCommon || showPermission ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (OverlayAnchorAdvanced != null)
|
||||
OverlayAnchorAdvanced.Visibility = showAdvanced ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (OverlayMaxContextTokensRow != null)
|
||||
OverlayMaxContextTokensRow.Visibility = showAdvanced ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (OverlayMaxRetryRow != null)
|
||||
OverlayMaxRetryRow.Visibility = showAdvanced ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (OverlayAdvancedTogglePanel != null)
|
||||
OverlayAdvancedTogglePanel.Visibility = showAdvanced ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void RefreshOverlaySettingsPanel()
|
||||
{
|
||||
if (CmbOverlayService == null || CmbOverlayModel == null)
|
||||
return;
|
||||
|
||||
var llm = _settings.Settings.Llm;
|
||||
var service = (llm.Service ?? "ollama").ToLowerInvariant();
|
||||
var models = GetModelCandidates(service);
|
||||
|
||||
_isOverlaySettingsSyncing = true;
|
||||
try
|
||||
{
|
||||
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));
|
||||
|
||||
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;
|
||||
|
||||
if (TxtOverlayModelInput != null)
|
||||
TxtOverlayModelInput.Text = llm.Model ?? "";
|
||||
if (ChkOverlayAiEnabled != null)
|
||||
ChkOverlayAiEnabled.IsChecked = _settings.Settings.AiEnabled;
|
||||
if (TxtOverlayServiceEndpoint != null)
|
||||
TxtOverlayServiceEndpoint.Text = GetOverlayServiceEndpoint(service);
|
||||
if (TxtOverlayServiceApiKey != null)
|
||||
TxtOverlayServiceApiKey.Password = GetOverlayServiceApiKey(service);
|
||||
if (ChkOverlayVllmAllowInsecureTls != null)
|
||||
ChkOverlayVllmAllowInsecureTls.IsChecked = llm.VllmAllowInsecureTls;
|
||||
if (TxtOverlayContextCompactTriggerPercent != null)
|
||||
TxtOverlayContextCompactTriggerPercent.Text = Math.Clamp(llm.ContextCompactTriggerPercent, 10, 95).ToString();
|
||||
if (TxtOverlayMaxContextTokens != null)
|
||||
TxtOverlayMaxContextTokens.Text = Math.Max(1024, llm.MaxContextTokens).ToString();
|
||||
if (TxtOverlayMaxRetryOnError != null)
|
||||
TxtOverlayMaxRetryOnError.Text = Math.Clamp(llm.MaxRetryOnError, 0, 10).ToString();
|
||||
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;
|
||||
if (ChkOverlayEnableCodeVerification != null)
|
||||
ChkOverlayEnableCodeVerification.IsChecked = llm.Code.EnableCodeVerification;
|
||||
if (ChkOverlayEnableParallelTools != null)
|
||||
ChkOverlayEnableParallelTools.IsChecked = llm.EnableParallelTools;
|
||||
|
||||
RefreshOverlayThemeCards();
|
||||
RefreshOverlayServiceCards();
|
||||
RefreshOverlayModeButtons();
|
||||
RefreshOverlayServiceFieldLabels(service);
|
||||
BuildOverlayModelChips(service);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isOverlaySettingsSyncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (candidates.Count > 0 && !candidates.Any(m => m.Id == llm.Model))
|
||||
llm.Model = candidates[0].Id;
|
||||
|
||||
_settings.Save();
|
||||
_appState.LoadFromSettings(_settings);
|
||||
UpdateModelLabel();
|
||||
RefreshInlineSettingsPanel();
|
||||
RefreshOverlaySettingsPanel();
|
||||
}
|
||||
|
||||
private void CmbOverlayModel_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_isOverlaySettingsSyncing || CmbOverlayModel.SelectedItem is not ComboBoxItem modelItem || modelItem.Tag is not string modelId)
|
||||
return;
|
||||
|
||||
_settings.Settings.Llm.Model = modelId;
|
||||
_settings.Save();
|
||||
_appState.LoadFromSettings(_settings);
|
||||
UpdateModelLabel();
|
||||
RefreshInlineSettingsPanel();
|
||||
}
|
||||
|
||||
private void RefreshOverlayThemeCards()
|
||||
{
|
||||
var selected = (_settings.Settings.Llm.AgentTheme ?? "system").ToLowerInvariant();
|
||||
SetOverlayCardSelection(OverlayThemeSystemCard, selected == "system");
|
||||
SetOverlayCardSelection(OverlayThemeLightCard, selected == "light");
|
||||
SetOverlayCardSelection(OverlayThemeDarkCard, selected == "dark");
|
||||
}
|
||||
|
||||
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 RefreshOverlayModeButtons()
|
||||
{
|
||||
var llm = _settings.Settings.Llm;
|
||||
BtnOverlayOperationMode.Content = OperationModePolicy.Normalize(_settings.Settings.OperationMode) == OperationModePolicy.ExternalMode
|
||||
? "사외 모드"
|
||||
: "사내 모드";
|
||||
BtnOverlayFolderDataUsage.Content = _folderDataUsage switch
|
||||
{
|
||||
"active" => "적극 활용",
|
||||
"passive" => "소극 활용",
|
||||
_ => "활용하지 않음",
|
||||
};
|
||||
BtnOverlayPermission.Content = PermissionModeCatalog.ToDisplayLabel(PermissionModeCatalog.NormalizeGlobalMode(llm.FilePermission));
|
||||
BtnOverlayPlanMode.Content = PlanModeLabel(llm.PlanMode);
|
||||
BtnOverlayReasoning.Content = ReasoningLabel(llm.AgentDecisionLevel);
|
||||
}
|
||||
|
||||
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 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 BuildOverlayModelChips(string service)
|
||||
{
|
||||
if (OverlayModelChipPanel == null)
|
||||
return;
|
||||
|
||||
OverlayModelChipPanel.Children.Clear();
|
||||
foreach (var model in GetModelCandidates(service))
|
||||
{
|
||||
var captured = model.Id;
|
||||
var border = new Border
|
||||
{
|
||||
Cursor = Cursors.Hand,
|
||||
CornerRadius = new CornerRadius(10),
|
||||
BorderThickness = new Thickness(1),
|
||||
BorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
|
||||
Background = Brushes.Transparent,
|
||||
Padding = new Thickness(10, 6, 10, 6),
|
||||
Margin = new Thickness(0, 0, 8, 8),
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = model.Label,
|
||||
FontSize = 11,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
|
||||
}
|
||||
};
|
||||
border.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
if (TxtOverlayModelInput != null)
|
||||
TxtOverlayModelInput.Text = captured;
|
||||
};
|
||||
OverlayModelChipPanel.Children.Add(border);
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
RefreshOverlayThemeCards();
|
||||
}
|
||||
|
||||
private void OverlayThemeLightCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_settings.Settings.Llm.AgentTheme = "light";
|
||||
RefreshOverlayThemeCards();
|
||||
}
|
||||
|
||||
private void OverlayThemeDarkCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_settings.Settings.Llm.AgentTheme = "dark";
|
||||
RefreshOverlayThemeCards();
|
||||
}
|
||||
|
||||
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;
|
||||
RefreshOverlaySettingsPanel();
|
||||
}
|
||||
|
||||
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;
|
||||
RefreshOverlayModeButtons();
|
||||
}
|
||||
|
||||
private void BtnOverlayFolderDataUsage_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_folderDataUsage = _folderDataUsage switch
|
||||
{
|
||||
"none" => "passive",
|
||||
"passive" => "active",
|
||||
_ => "none",
|
||||
};
|
||||
RefreshOverlayModeButtons();
|
||||
}
|
||||
|
||||
private void BtnOverlaySave_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var llm = _settings.Settings.Llm;
|
||||
var service = (llm.Service ?? "ollama").Trim().ToLowerInvariant();
|
||||
var endpoint = TxtOverlayServiceEndpoint?.Text.Trim() ?? "";
|
||||
var apiKey = TxtOverlayServiceApiKey?.Password ?? "";
|
||||
|
||||
_settings.Settings.AiEnabled = ChkOverlayAiEnabled?.IsChecked == true;
|
||||
llm.Model = TxtOverlayModelInput?.Text.Trim() ?? llm.Model;
|
||||
llm.VllmAllowInsecureTls = ChkOverlayVllmAllowInsecureTls?.IsChecked == true;
|
||||
llm.EnableProactiveContextCompact = ChkOverlayEnableProactiveCompact?.IsChecked == true;
|
||||
llm.ContextCompactTriggerPercent = ParseOverlayInt(TxtOverlayContextCompactTriggerPercent?.Text, 80, 10, 95);
|
||||
llm.MaxContextTokens = ParseOverlayInt(TxtOverlayMaxContextTokens?.Text, 4096, 1024, 200000);
|
||||
llm.MaxRetryOnError = ParseOverlayInt(TxtOverlayMaxRetryOnError?.Text, 3, 0, 10);
|
||||
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.Code.EnableCodeVerification = ChkOverlayEnableCodeVerification?.IsChecked == true;
|
||||
llm.EnableParallelTools = ChkOverlayEnableParallelTools?.IsChecked == true;
|
||||
llm.FolderDataUsage = _folderDataUsage;
|
||||
llm.AgentUiExpressionLevel = "rich";
|
||||
|
||||
switch (service)
|
||||
{
|
||||
case "ollama":
|
||||
llm.OllamaEndpoint = endpoint;
|
||||
llm.OllamaApiKey = apiKey;
|
||||
llm.Endpoint = endpoint;
|
||||
llm.ApiKey = apiKey;
|
||||
llm.OllamaModel = llm.Model;
|
||||
break;
|
||||
case "vllm":
|
||||
llm.VllmEndpoint = endpoint;
|
||||
llm.VllmApiKey = apiKey;
|
||||
llm.Endpoint = endpoint;
|
||||
llm.ApiKey = apiKey;
|
||||
llm.VllmModel = llm.Model;
|
||||
break;
|
||||
case "gemini":
|
||||
llm.Endpoint = endpoint;
|
||||
llm.GeminiApiKey = apiKey;
|
||||
llm.ApiKey = apiKey;
|
||||
llm.GeminiModel = llm.Model;
|
||||
break;
|
||||
case "claude":
|
||||
case "sigmoid":
|
||||
llm.Endpoint = endpoint;
|
||||
llm.ClaudeApiKey = apiKey;
|
||||
llm.ApiKey = apiKey;
|
||||
llm.ClaudeModel = llm.Model;
|
||||
break;
|
||||
}
|
||||
|
||||
_settings.Save();
|
||||
_appState.LoadFromSettings(_settings);
|
||||
ApplyAgentThemeResources();
|
||||
UpdatePermissionUI();
|
||||
UpdateDataUsageUI();
|
||||
SaveConversationSettings();
|
||||
RefreshInlineSettingsPanel();
|
||||
UpdateModelLabel();
|
||||
UpdateTabUI();
|
||||
AgentSettingsOverlay.Visibility = Visibility.Collapsed;
|
||||
ShowToast("AX Agent 설정이 저장되었습니다.");
|
||||
}
|
||||
|
||||
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.Visibility = Visibility.Collapsed;
|
||||
|
||||
@@ -13108,6 +13603,7 @@ public partial class ChatWindow : Window
|
||||
_settings.Save();
|
||||
_appState.LoadFromSettings(_settings);
|
||||
RefreshInlineSettingsPanel();
|
||||
RefreshOverlaySettingsPanel();
|
||||
}
|
||||
|
||||
private void BtnInlineReasoning_Click(object sender, RoutedEventArgs e)
|
||||
@@ -13117,6 +13613,7 @@ public partial class ChatWindow : Window
|
||||
_settings.Save();
|
||||
_appState.LoadFromSettings(_settings);
|
||||
RefreshInlineSettingsPanel();
|
||||
RefreshOverlaySettingsPanel();
|
||||
}
|
||||
|
||||
private void BtnInlinePlanMode_Click(object sender, RoutedEventArgs e)
|
||||
@@ -13126,6 +13623,7 @@ public partial class ChatWindow : Window
|
||||
_settings.Save();
|
||||
_appState.LoadFromSettings(_settings);
|
||||
RefreshInlineSettingsPanel();
|
||||
RefreshOverlaySettingsPanel();
|
||||
}
|
||||
|
||||
private void BtnInlinePermission_Click(object sender, RoutedEventArgs e)
|
||||
@@ -13137,6 +13635,7 @@ public partial class ChatWindow : Window
|
||||
UpdatePermissionUI();
|
||||
SaveConversationSettings();
|
||||
RefreshInlineSettingsPanel();
|
||||
RefreshOverlaySettingsPanel();
|
||||
}
|
||||
|
||||
private void BtnInlineSkill_Click(object sender, RoutedEventArgs e)
|
||||
|
||||
Reference in New Issue
Block a user