권한 코어를 claude-code 기준으로 재구성하고 slash palette 상태 분리를 시작\n\n- Default/AcceptEdits/Plan/BypassPermissions/DontAsk/Deny 권한 모드를 추가하고 기존 Ask/Auto 호환을 유지\n- deny 우선 패턴 규칙, allow/override, 글로벌 모드 순서의 권한 해석 체계를 정리\n- file_write/file_edit/file_manage와 process/build_run/test_loop/snippet_runner/spawn_agent 계열을 권한 클래스별로 분리\n- AcceptEdits는 파일 편집 도구 자동 허용, process 계열은 계속 확인하도록 조정\n- Plan은 쓰기 도구를 차단하고 읽기 중심 진행이 되도록 보강\n- BypassPermissions와 DontAsk는 권한 확인을 생략하는 경로로 정규화\n- AX Agent 권한 팝업, 상단 배너, slash 명령 결과를 새 권한 체계에 맞게 정리\n- /permissions, /allowed-tools, /sandbox-toggle 사용법과 상태 출력을 갱신\n- ChatWindow의 slash palette 상태를 전용 SlashPaletteState로 분리해 이후 composer 개편 기반을 마련\n- AppState, 설정 모델, 테스트를 새 권한 체계에 맞게 갱신\n- dotnet build 경고 0 / 오류 0, dotnet test 436 통과를 확인
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:
383
src/AxCopilot/Views/AgentSettingsWindow.xaml.cs
Normal file
383
src/AxCopilot/Views/AgentSettingsWindow.xaml.cs
Normal file
@@ -0,0 +1,383 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class AgentSettingsWindow : Window
|
||||
{
|
||||
private const string UnifiedAdminPassword = "axgo123!";
|
||||
|
||||
private readonly SettingsService _settings;
|
||||
private readonly LlmSettings _llm;
|
||||
private string _permissionMode = PermissionModeCatalog.Ask;
|
||||
private string _planMode = "off";
|
||||
private string _reasoningMode = "detailed";
|
||||
private string _folderDataUsage = "active";
|
||||
private string _operationMode = OperationModePolicy.InternalMode;
|
||||
|
||||
public AgentSettingsWindow(SettingsService settings)
|
||||
{
|
||||
_settings = settings;
|
||||
_llm = _settings.Settings.Llm;
|
||||
InitializeComponent();
|
||||
LoadFromSettings();
|
||||
}
|
||||
|
||||
private void LoadFromSettings()
|
||||
{
|
||||
ModelInput.Text = _llm.Model ?? "";
|
||||
_permissionMode = PermissionModeCatalog.NormalizeGlobalMode(_llm.FilePermission);
|
||||
_planMode = string.IsNullOrWhiteSpace(_llm.PlanMode) ? "off" : _llm.PlanMode;
|
||||
_reasoningMode = string.IsNullOrWhiteSpace(_llm.AgentDecisionLevel) ? "detailed" : _llm.AgentDecisionLevel;
|
||||
_folderDataUsage = string.IsNullOrWhiteSpace(_llm.FolderDataUsage) ? "active" : _llm.FolderDataUsage;
|
||||
_operationMode = OperationModePolicy.Normalize(_settings.Settings.OperationMode);
|
||||
|
||||
ChkVllmAllowInsecureTls.IsChecked = _llm.VllmAllowInsecureTls;
|
||||
ChkEnableProactiveCompact.IsChecked = _llm.EnableProactiveContextCompact;
|
||||
TxtContextCompactTriggerPercent.Text = Math.Clamp(_llm.ContextCompactTriggerPercent, 10, 95).ToString();
|
||||
TxtMaxContextTokens.Text = Math.Max(1024, _llm.MaxContextTokens).ToString();
|
||||
TxtMaxRetryOnError.Text = Math.Clamp(_llm.MaxRetryOnError, 0, 10).ToString();
|
||||
|
||||
ChkEnableSkillSystem.IsChecked = _llm.EnableSkillSystem;
|
||||
ChkEnableToolHooks.IsChecked = _llm.EnableToolHooks;
|
||||
ChkEnableHookInputMutation.IsChecked = _llm.EnableHookInputMutation;
|
||||
ChkEnableHookPermissionUpdate.IsChecked = _llm.EnableHookPermissionUpdate;
|
||||
ChkEnableCoworkVerification.IsChecked = _llm.EnableCoworkVerification;
|
||||
ChkEnableCodeVerification.IsChecked = _llm.Code.EnableCodeVerification;
|
||||
ChkEnableParallelTools.IsChecked = _llm.EnableParallelTools;
|
||||
|
||||
RefreshServiceCards();
|
||||
RefreshThemeCards();
|
||||
RefreshModeLabels();
|
||||
BuildModelChips();
|
||||
}
|
||||
|
||||
private void RefreshThemeCards()
|
||||
{
|
||||
var selected = (_llm.AgentTheme ?? "system").ToLowerInvariant();
|
||||
SetCardSelection(ThemeSystemCard, selected == "system");
|
||||
SetCardSelection(ThemeLightCard, selected == "light");
|
||||
SetCardSelection(ThemeDarkCard, selected == "dark");
|
||||
}
|
||||
|
||||
private void RefreshServiceCards()
|
||||
{
|
||||
var service = (_llm.Service ?? "ollama").ToLowerInvariant();
|
||||
SetCardSelection(SvcOllamaCard, service == "ollama");
|
||||
SetCardSelection(SvcVllmCard, service == "vllm");
|
||||
SetCardSelection(SvcGeminiCard, service == "gemini");
|
||||
SetCardSelection(SvcClaudeCard, service is "claude" or "sigmoid");
|
||||
}
|
||||
|
||||
private void RefreshModeLabels()
|
||||
{
|
||||
BtnOperationMode.Content = BuildOperationModeLabel(_operationMode);
|
||||
BtnPermissionMode.Content = PermissionModeCatalog.ToDisplayLabel(_permissionMode);
|
||||
BtnPlanMode.Content = _planMode;
|
||||
BtnReasoningMode.Content = _reasoningMode;
|
||||
BtnFolderDataUsage.Content = _folderDataUsage;
|
||||
AdvancedPanel.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private static string BuildOperationModeLabel(string mode)
|
||||
{
|
||||
return OperationModePolicy.Normalize(mode) == OperationModePolicy.ExternalMode
|
||||
? "사외 모드"
|
||||
: "사내 모드";
|
||||
}
|
||||
|
||||
private void SetCardSelection(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 BuildModelChips()
|
||||
{
|
||||
ModelChipPanel.Children.Clear();
|
||||
var models = GetModelCandidates(_llm.Service);
|
||||
foreach (var model in models)
|
||||
{
|
||||
var captured = model;
|
||||
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,
|
||||
FontSize = 11,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
},
|
||||
};
|
||||
border.MouseLeftButtonUp += (_, _) => ModelInput.Text = captured;
|
||||
ModelChipPanel.Children.Add(border);
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> GetModelCandidates(string? service)
|
||||
{
|
||||
var key = (service ?? "ollama").ToLowerInvariant();
|
||||
var result = new List<string>();
|
||||
|
||||
foreach (var m in _llm.RegisteredModels)
|
||||
{
|
||||
if (!string.Equals(m.Service, key, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
if (!string.IsNullOrWhiteSpace(m.Alias) && !result.Contains(m.Alias))
|
||||
result.Add(m.Alias);
|
||||
}
|
||||
|
||||
void AddIf(string? value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value) && !result.Contains(value))
|
||||
result.Add(value);
|
||||
}
|
||||
|
||||
if (key == "ollama") AddIf(_llm.OllamaModel);
|
||||
else if (key == "vllm") AddIf(_llm.VllmModel);
|
||||
else if (key == "gemini") AddIf(_llm.GeminiModel);
|
||||
else AddIf(_llm.ClaudeModel);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void SetService(string service)
|
||||
{
|
||||
_llm.Service = service;
|
||||
RefreshServiceCards();
|
||||
BuildModelChips();
|
||||
}
|
||||
|
||||
private void SetTheme(string theme)
|
||||
{
|
||||
_llm.AgentTheme = theme;
|
||||
RefreshThemeCards();
|
||||
}
|
||||
|
||||
private static string CycleOperationMode(string current)
|
||||
{
|
||||
return OperationModePolicy.Normalize(current) == OperationModePolicy.ExternalMode
|
||||
? OperationModePolicy.InternalMode
|
||||
: OperationModePolicy.ExternalMode;
|
||||
}
|
||||
|
||||
private bool PromptPasswordDialog(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 void ThemeSystemCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetTheme("system");
|
||||
private void ThemeLightCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetTheme("light");
|
||||
private void ThemeDarkCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetTheme("dark");
|
||||
|
||||
private void SvcOllamaCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetService("ollama");
|
||||
private void SvcVllmCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetService("vllm");
|
||||
private void SvcGeminiCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetService("gemini");
|
||||
private void SvcClaudeCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => SetService("claude");
|
||||
|
||||
private void BtnOperationMode_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var next = CycleOperationMode(_operationMode);
|
||||
if (!PromptPasswordDialog(
|
||||
"운영 모드 변경",
|
||||
"사내/사외 모드 변경",
|
||||
"비밀번호를 입력하세요:"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_operationMode = next;
|
||||
RefreshModeLabels();
|
||||
}
|
||||
|
||||
private void BtnPermissionMode_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_permissionMode = PermissionModeCatalog.NormalizeGlobalMode(_permissionMode) switch
|
||||
{
|
||||
PermissionModeCatalog.Deny => PermissionModeCatalog.Default,
|
||||
PermissionModeCatalog.Default => PermissionModeCatalog.AcceptEdits,
|
||||
PermissionModeCatalog.AcceptEdits => PermissionModeCatalog.Plan,
|
||||
PermissionModeCatalog.Plan => PermissionModeCatalog.BypassPermissions,
|
||||
PermissionModeCatalog.BypassPermissions => PermissionModeCatalog.DontAsk,
|
||||
_ => PermissionModeCatalog.Deny,
|
||||
};
|
||||
RefreshModeLabels();
|
||||
}
|
||||
|
||||
private void BtnPlanMode_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_planMode = _planMode switch
|
||||
{
|
||||
"off" => "auto",
|
||||
"auto" => "always",
|
||||
_ => "off",
|
||||
};
|
||||
RefreshModeLabels();
|
||||
}
|
||||
|
||||
private void BtnReasoningMode_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_reasoningMode = _reasoningMode switch
|
||||
{
|
||||
"minimal" => "normal",
|
||||
"normal" => "detailed",
|
||||
_ => "minimal",
|
||||
};
|
||||
RefreshModeLabels();
|
||||
}
|
||||
|
||||
private void BtnFolderDataUsage_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_folderDataUsage = _folderDataUsage switch
|
||||
{
|
||||
"none" => "passive",
|
||||
"passive" => "active",
|
||||
_ => "none",
|
||||
};
|
||||
RefreshModeLabels();
|
||||
}
|
||||
|
||||
private void BtnSave_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_llm.Model = ModelInput.Text.Trim();
|
||||
_llm.FilePermission = _permissionMode;
|
||||
_llm.PlanMode = _planMode;
|
||||
_llm.AgentDecisionLevel = _reasoningMode;
|
||||
_llm.FolderDataUsage = _folderDataUsage;
|
||||
_llm.AgentUiExpressionLevel = "rich";
|
||||
|
||||
_llm.VllmAllowInsecureTls = ChkVllmAllowInsecureTls.IsChecked == true;
|
||||
_llm.EnableProactiveContextCompact = ChkEnableProactiveCompact.IsChecked == true;
|
||||
_llm.ContextCompactTriggerPercent = ParseInt(TxtContextCompactTriggerPercent.Text, 80, 10, 95);
|
||||
_llm.MaxContextTokens = ParseInt(TxtMaxContextTokens.Text, 4096, 1024, 200000);
|
||||
_llm.MaxRetryOnError = ParseInt(TxtMaxRetryOnError.Text, 3, 0, 10);
|
||||
|
||||
_llm.EnableSkillSystem = ChkEnableSkillSystem.IsChecked == true;
|
||||
_llm.EnableToolHooks = ChkEnableToolHooks.IsChecked == true;
|
||||
_llm.EnableHookInputMutation = ChkEnableHookInputMutation.IsChecked == true;
|
||||
_llm.EnableHookPermissionUpdate = ChkEnableHookPermissionUpdate.IsChecked == true;
|
||||
_llm.EnableCoworkVerification = ChkEnableCoworkVerification.IsChecked == true;
|
||||
_llm.Code.EnableCodeVerification = ChkEnableCodeVerification.IsChecked == true;
|
||||
_llm.EnableParallelTools = ChkEnableParallelTools.IsChecked == true;
|
||||
|
||||
_settings.Settings.OperationMode = OperationModePolicy.Normalize(_operationMode);
|
||||
_settings.Save();
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void BtnOpenFullSettings_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (System.Windows.Application.Current is App app)
|
||||
app.OpenSettingsFromChat();
|
||||
}
|
||||
|
||||
private void BtnClose_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) => Close();
|
||||
|
||||
private static int ParseInt(string? text, int fallback, int min, int max)
|
||||
{
|
||||
if (!int.TryParse(text, out var value))
|
||||
value = fallback;
|
||||
return Math.Clamp(value, min, max);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
23
src/AxCopilot/Views/SlashPaletteState.cs
Normal file
23
src/AxCopilot/Views/SlashPaletteState.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
internal sealed class SlashPaletteState
|
||||
{
|
||||
public List<(string Cmd, string Label, bool IsSkill)> Matches { get; set; } = [];
|
||||
|
||||
public int SelectedIndex { get; set; } = -1;
|
||||
|
||||
public string? ActiveCommand { get; set; }
|
||||
|
||||
public void ResetMatches(List<(string Cmd, string Label, bool IsSkill)> matches)
|
||||
{
|
||||
Matches = matches;
|
||||
SelectedIndex = -1;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Matches = [];
|
||||
SelectedIndex = -1;
|
||||
ActiveCommand = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user