diff --git a/README.md b/README.md index f6bf0a4..f0a3c22 100644 --- a/README.md +++ b/README.md @@ -222,7 +222,7 @@ public class MyHandler : IActionHandler ### v0.7.3 — AX Agent 권한 코어 재구성 + 입력 계층 정리 -업데이트: 2026-04-04 12:11 (KST) +업데이트: 2026-04-04 12:22 (KST) | 분류 | 내용 | |------|------| @@ -264,6 +264,9 @@ public class MyHandler : IActionHandler | 슬래시 탐색 입력 확장 | `/` 팝업에서 휠/방향키 외에 `PageUp/PageDown/Home/End` 이동을 추가하고 고해상도 휠 델타를 단계 이동으로 보정해 스크롤 사용성을 개선 | | 모델 빠른설정 단일 라인 강화 | 입력창 상단 모델 버튼을 AX Agent 내부 빠른 설정 토글로 전환하고, 모델/프리셋 버튼 높이와 패딩을 정돈해 Codex/Claude형 단일 라인 흐름에 맞춤 | | UI 점검 체크리스트 추가 | 내부/사외 모드 포함 UI 회귀 점검 문서를 `docs/UI_UX_CHECKLIST.md`로 추가해 시나리오 기반 검증 기준을 명문화 | +| 권한 모드 표면 통일 | 권한 표시 명칭을 `활용하지 않음/소극 활용/적극 활용/계획 중심/완전 자동/질문 없이 진행`으로 통일하고 팝업 선택 순서를 동일 체계로 재정렬 | +| 권한 기본 동작/순환 보강 | Chat 탭 기본 권한을 `활용하지 않음`으로 적용하고, `/sandbox-toggle` 및 AX Agent 설정 권한 순환을 같은 순서(`활용하지 않음→소극→적극→계획→완전 자동→질문 없이 진행`)로 맞춤 | +| 운영 모드 회귀 점검 강화 | `OperationModePolicyTests`, `OperationModeReadinessTests`, `LlmOperationModeTests` 필터 테스트(18건)를 통과해 internal/external 차단·허용 경로를 재검증 | | Slash palette 상태 분리 시작 | `ChatWindow`에 몰려 있던 slash 상태를 `SlashPaletteState`로 분리해 이후 Codex/Claude형 composer 개편 기반 마련 | | 런처 이미지 미리보기 추가 | `#` 클립보드 이미지 항목에서 `Shift+Enter`로 전용 미리보기 창을 열고, 줌·원본 해상도 확인·PNG/JPEG/BMP 저장·클립보드 복사를 지원 | | 검증 | `dotnet build` 경고 0 / 오류 0, `dotnet test` 436 passed / 0 failed | diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 2cc01b2..0d0c920 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -3170,6 +3170,42 @@ else: - `docs/UI_UX_CHECKLIST.md` 신규 추가. - 공통 품질, 슬래시, 권한, 설정, 내부/사외 운영 모드, 빌드/테스트까지 시나리오 점검 기준을 문서화. +## 2026-04-04 추가 진행 기록 (연속 실행 22차: 권한 체계 표면 통일 + 운영 모드 점검) + +업데이트: 2026-04-04 12:22 (KST) + +### 1) 권한 표면 명칭/순서 통일 +- `PermissionModeCatalog` 표시 라벨을 아래 체계로 통일: + - `Deny` → `활용하지 않음` + - `Default` → `소극 활용` + - `AcceptEdits` → `적극 활용` + - `Plan` → `계획 중심` + - `BypassPermissions` → `완전 자동` + - `DontAsk` → `질문 없이 진행` +- 권한 팝업 선택 순서를 동일 체계로 재정렬하고, 기존 고급 섹션 분리를 줄여 단일 흐름 탐색으로 정리. + +### 2) 권한 UI/동작 연결 정비 +- 하단/배너/팝업에서 권한 라벨과 색상 체계를 일치시킴. +- `Chat` 탭 기본 권한 적용 경로를 `활용하지 않음`으로 조정. +- `/sandbox-toggle`와 `AgentSettingsWindow` 권한 순환 순서를 동일 체계로 통일. +- 작업 요약 카드의 권한 액션 버튼 라벨도 새 체계로 맞춤. + +### 3) 좌측 패널 단순화 보강 +- 좌측 패널의 모드 배지 헤더를 숨겨 시각 복잡도를 낮추고, 탭별 핵심 선택(주제/작업) 중심으로 정리. + +### 4) 운영 모드(사내/사외) 회귀 점검 +- 코드 경로 확인: + - `HttpTool`, `OpenExternalTool`, `OperationModePolicy` 기반 차단 로직 유지 확인. +- 테스트 필터 실행: + - `OperationModePolicyTests` + - `OperationModeReadinessTests` + - `LlmOperationModeTests` +- 결과: 18/18 통과. + +### 5) 품질 게이트 +- `dotnet build src/AxCopilot/AxCopilot.csproj` 통과 (경고 0, 오류 0). +- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj` 통과 (436 passed, 0 failed). + diff --git a/src/AxCopilot/Services/Agent/PermissionModeCatalog.cs b/src/AxCopilot/Services/Agent/PermissionModeCatalog.cs index 70e21df..6dcea6f 100644 --- a/src/AxCopilot/Services/Agent/PermissionModeCatalog.cs +++ b/src/AxCopilot/Services/Agent/PermissionModeCatalog.cs @@ -17,12 +17,12 @@ public static class PermissionModeCatalog public static readonly IReadOnlyList UserSelectableModes = new[] { + Deny, Default, AcceptEdits, Plan, BypassPermissions, DontAsk, - Deny, }; /// @@ -107,9 +107,9 @@ public static class PermissionModeCatalog { Default => "소극 활용", AcceptEdits => "적극 활용", - Plan => "Plan", - BypassPermissions => "Bypass", - DontAsk => "DontAsk", + Plan => "계획 중심", + BypassPermissions => "완전 자동", + DontAsk => "질문 없이 진행", Deny => "활용하지 않음", _ => normalized, }; diff --git a/src/AxCopilot/Views/AgentSettingsWindow.xaml.cs b/src/AxCopilot/Views/AgentSettingsWindow.xaml.cs index c4f7132..559eba2 100644 --- a/src/AxCopilot/Views/AgentSettingsWindow.xaml.cs +++ b/src/AxCopilot/Views/AgentSettingsWindow.xaml.cs @@ -1,4 +1,4 @@ -using System.Windows; +using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; @@ -14,7 +14,7 @@ public partial class AgentSettingsWindow : Window private readonly SettingsService _settings; private readonly LlmSettings _llm; - private string _permissionMode = PermissionModeCatalog.Ask; + private string _permissionMode = PermissionModeCatalog.Deny; private string _planMode = "off"; private string _reasoningMode = "detailed"; private string _folderDataUsage = "active"; @@ -78,9 +78,9 @@ public partial class AgentSettingsWindow : Window { BtnOperationMode.Content = BuildOperationModeLabel(_operationMode); BtnPermissionMode.Content = PermissionModeCatalog.ToDisplayLabel(_permissionMode); - BtnPlanMode.Content = _planMode; - BtnReasoningMode.Content = _reasoningMode; - BtnFolderDataUsage.Content = _folderDataUsage; + BtnPlanMode.Content = BuildPlanModeLabel(_planMode); + BtnReasoningMode.Content = BuildReasoningModeLabel(_reasoningMode); + BtnFolderDataUsage.Content = BuildFolderDataUsageLabel(_folderDataUsage); AdvancedPanel.Visibility = Visibility.Visible; } @@ -91,6 +91,36 @@ public partial class AgentSettingsWindow : Window : "사내 모드"; } + private static string BuildPlanModeLabel(string mode) + { + return (mode ?? "off").ToLowerInvariant() switch + { + "always" => "항상 계획", + "auto" => "자동 계획", + _ => "끄기", + }; + } + + private static string BuildReasoningModeLabel(string mode) + { + return (mode ?? "detailed").ToLowerInvariant() switch + { + "minimal" => "낮음", + "normal" => "중간", + _ => "높음", + }; + } + + private static string BuildFolderDataUsageLabel(string mode) + { + return (mode ?? "none").ToLowerInvariant() switch + { + "active" => "적극 활용", + "passive" => "소극 활용", + _ => "활용하지 않음", + }; + } + private void SetCardSelection(Border border, bool selected) { var accent = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue; @@ -281,7 +311,7 @@ public partial class AgentSettingsWindow : Window if (!PromptPasswordDialog( "운영 모드 변경", "사내/사외 모드 변경", - "비밀번호를 입력하세요:")) + "비밀번호를 입력하세요.")) { return; } diff --git a/src/AxCopilot/Views/ChatWindow.xaml b/src/AxCopilot/Views/ChatWindow.xaml index 7061666..73ce2a6 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml +++ b/src/AxCopilot/Views/ChatWindow.xaml @@ -306,7 +306,7 @@ CornerRadius="10" Padding="10,8"> - + - diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index c74177b..dfd4faf 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -1879,8 +1879,8 @@ public partial class ChatWindow : Window return button; } - actionRow.Children.Add(CreateActionButton("권한 요청", "#F8FAFC", "#334155", () => SetToolPermissionOverride(latestDenied.ToolName!, PermissionModeCatalog.Default))); - actionRow.Children.Add(CreateActionButton("편집 자동 승인", "#FFF7ED", "#C2410C", () => SetToolPermissionOverride(latestDenied.ToolName!, PermissionModeCatalog.AcceptEdits))); + 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("예외 해제", "#F3F4F6", "#374151", () => SetToolPermissionOverride(latestDenied.ToolName!, null))); deniedStack.Children.Add(actionRow); @@ -1899,15 +1899,12 @@ public partial class ChatWindow : Window } var coreLevels = new (string Level, string Sym, string Title, string Desc, string Color)[] - { - (PermissionModeCatalog.Default, "\uE8D7", "권한 요청", "변경하기 전에 항상 확인", "#4B5EFC"), - (PermissionModeCatalog.AcceptEdits, "\uE70F", "편집 자동 승인", "모든 파일 편집 자동 승인", "#DD6B20"), - (PermissionModeCatalog.Plan, "\uE7C3", "계획 모드", "변경하기 전에 계획 만들기", "#4338CA"), - (PermissionModeCatalog.BypassPermissions, "\uE814", "권한 건너뛰기", "모든 권한 허용", "#166534"), - }; - var advancedLevels = new (string Level, string Sym, string Title, string Desc, string Color)[] { (PermissionModeCatalog.Deny, "\uE711", "활용하지 않음", "파일 읽기만 허용하고 생성/수정/삭제를 차단합니다", "#107C10"), + (PermissionModeCatalog.Default, "\uE8D7", "소극 활용", "변경 전 확인하며 필요할 때만 파일 접근을 진행합니다", "#2563EB"), + (PermissionModeCatalog.AcceptEdits, "\uE73E", "적극 활용", "파일 편집 도구를 자동 승인하고 명령 실행은 계속 확인합니다", "#107C10"), + (PermissionModeCatalog.Plan, "\uE7C3", "계획 중심", "쓰기 전 계획/승인 흐름을 우선합니다", "#4338CA"), + (PermissionModeCatalog.BypassPermissions, "\uE814", "완전 자동", "권한 확인을 대부분 생략합니다. 민감 작업에 주의하세요", "#B45309"), (PermissionModeCatalog.DontAsk, "\uE8A5", "질문 없이 진행", "권한 질문 없이 진행합니다. 자동 실행 범위를 점검하세요", "#B91C1C"), }; var current = PermissionModeCatalog.NormalizeGlobalMode(_settings.Settings.Llm.FilePermission); @@ -2017,7 +2014,7 @@ public partial class ChatWindow : Window PermissionItems.Children.Add(new TextBlock { - Text = "핵심 권한 모드", + Text = "권한 모드", FontSize = 10.5, FontWeight = FontWeights.SemiBold, Foreground = secondaryText, @@ -2048,15 +2045,6 @@ public partial class ChatWindow : Window expanded: GetPermissionPopupSectionExpanded("permission_denied", false), accentHex: "#991B1B")); - var advancedPanel = new StackPanel(); - AddPermissionRows(advancedPanel, advancedLevels); - PermissionItems.Children.Add(CreateCollapsibleSection( - "permission_advanced", - "\uE9CE", - "고급 모드", - advancedPanel, - expanded: GetPermissionPopupSectionExpanded("permission_advanced", false), - accentHex: "#64748B")); PermissionPopup.IsOpen = true; Dispatcher.BeginInvoke(() => { @@ -2151,7 +2139,7 @@ public partial class ChatWindow : Window _ => "\uE8D7", }; if (BtnPermission != null) - BtnPermission.ToolTip = $"{summary.Description}\n기본값 {summary.DefaultMode} · override {summary.OverrideCount}개"; + BtnPermission.ToolTip = $"{summary.Description}\n기본값 {PermissionModeCatalog.ToDisplayLabel(summary.DefaultMode)} · override {summary.OverrideCount}개"; if (!string.Equals(_lastPermissionBannerMode, perm, StringComparison.OrdinalIgnoreCase)) { @@ -2162,17 +2150,17 @@ public partial class ChatWindow : Window // 모드별 색상 + 상단 권한 배너 표시 if (perm == PermissionModeCatalog.AcceptEdits) { - var warnColor = new SolidColorBrush(Color.FromRgb(0xDD, 0x6B, 0x20)); - PermissionLabel.Foreground = warnColor; - PermissionIcon.Foreground = warnColor; + var activeColor = new SolidColorBrush(Color.FromRgb(0x10, 0x7C, 0x10)); + PermissionLabel.Foreground = activeColor; + PermissionIcon.Foreground = activeColor; if (PermissionTopBanner != null) { - PermissionTopBanner.BorderBrush = BrushFromHex("#FDBA74"); - PermissionTopBannerIcon.Text = "\uE7BA"; - PermissionTopBannerIcon.Foreground = warnColor; - PermissionTopBannerTitle.Text = "현재 권한 모드 · 편집 자동 승인"; - PermissionTopBannerTitle.Foreground = BrushFromHex("#C2410C"); - PermissionTopBannerText.Text = "파일 편집 도구는 자동 허용하고 명령 실행은 계속 확인합니다."; + PermissionTopBanner.BorderBrush = BrushFromHex("#86EFAC"); + PermissionTopBannerIcon.Text = "\uE73E"; + PermissionTopBannerIcon.Foreground = activeColor; + PermissionTopBannerTitle.Text = "현재 권한 모드 · 적극 활용"; + PermissionTopBannerTitle.Foreground = BrushFromHex("#166534"); + PermissionTopBannerText.Text = "파일 편집 도구는 자동 승인하고, 명령 실행은 계속 확인합니다."; PermissionTopBanner.Visibility = _permissionTopBannerDismissed ? Visibility.Collapsed : Visibility.Visible; } } @@ -2210,11 +2198,11 @@ public partial class ChatWindow : Window } else { - var defaultFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; + var defaultFg = BrushFromHex("#2563EB"); var iconFg = perm switch { "Plan" => new SolidColorBrush(Color.FromRgb(0x43, 0x38, 0xCA)), - _ => new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)), + _ => new SolidColorBrush(Color.FromRgb(0x25, 0x63, 0xEB)), }; PermissionLabel.Foreground = defaultFg; PermissionIcon.Foreground = iconFg; @@ -2225,11 +2213,21 @@ 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; } + else if (perm == PermissionModeCatalog.Default) + { + PermissionTopBanner.BorderBrush = BrushFromHex("#BFDBFE"); + PermissionTopBannerIcon.Text = "\uE8D7"; + PermissionTopBannerIcon.Foreground = BrushFromHex("#1D4ED8"); + PermissionTopBannerTitle.Text = "현재 권한 모드 · 소극 활용"; + PermissionTopBannerTitle.Foreground = BrushFromHex("#1D4ED8"); + PermissionTopBannerText.Text = "변경 전 확인하고, 필요한 경우에만 파일 접근을 진행합니다."; + PermissionTopBanner.Visibility = _permissionTopBannerDismissed ? Visibility.Collapsed : Visibility.Visible; + } else { PermissionTopBanner.Visibility = Visibility.Collapsed; @@ -2244,10 +2242,12 @@ public partial class ChatWindow : Window var next = action switch { "ask" => PermissionModeCatalog.Default, + "passive" => PermissionModeCatalog.Default, "default" => PermissionModeCatalog.Default, "acceptedits" => PermissionModeCatalog.AcceptEdits, "accept" => PermissionModeCatalog.AcceptEdits, "auto" => PermissionModeCatalog.AcceptEdits, + "active" => PermissionModeCatalog.AcceptEdits, "deny" => PermissionModeCatalog.Deny, "plan" => PermissionModeCatalog.Plan, "bypass" => PermissionModeCatalog.BypassPermissions, @@ -2389,8 +2389,8 @@ public partial class ChatWindow : Window { if (_activeTab == "Chat") { - // Chat 탭: 경고 배너 숨기고 기본 Ask 모드로 복원 - _settings.Settings.Llm.FilePermission = PermissionModeCatalog.Ask; + // Chat 탭: 경고 배너 숨기고 기본 제한 모드로 복원 + _settings.Settings.Llm.FilePermission = PermissionModeCatalog.Deny; UpdatePermissionUI(); return; } @@ -12837,10 +12837,12 @@ public partial class ChatWindow : Window }; private static string NextPermission(string current) => PermissionModeCatalog.NormalizeGlobalMode(current).ToLowerInvariant() switch { + "deny" => "Default", "default" => "AcceptEdits", "acceptedits" => "Plan", "plan" => "BypassPermissions", - _ => "Default", + "bypasspermissions" => "DontAsk", + _ => "Deny", }; private static string ServiceLabel(string service) => (service ?? "").ToLowerInvariant() switch { @@ -16176,10 +16178,11 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi } actions.Children.Add(BuildPermissionButton("활용하지 않음", "#FEF2F2", "#FCA5A5", "#991B1B", PermissionModeCatalog.Deny)); - actions.Children.Add(BuildPermissionButton("권한 요청", "#F8FAFC", "#CBD5E1", "#334155", PermissionModeCatalog.Default)); - actions.Children.Add(BuildPermissionButton("편집 자동 승인", "#FFF7ED", "#FDBA74", "#C2410C", PermissionModeCatalog.AcceptEdits)); - actions.Children.Add(BuildPermissionButton("계획 모드", "#EEF2FF", "#C7D2FE", "#3730A3", PermissionModeCatalog.Plan)); - actions.Children.Add(BuildPermissionButton("권한 건너뛰기", "#ECFDF5", "#BBF7D0", "#166534", PermissionModeCatalog.BypassPermissions)); + actions.Children.Add(BuildPermissionButton("소극 활용", "#EFF6FF", "#BFDBFE", "#1D4ED8", PermissionModeCatalog.Default)); + actions.Children.Add(BuildPermissionButton("적극 활용", "#ECFDF5", "#BBF7D0", "#166534", PermissionModeCatalog.AcceptEdits)); + actions.Children.Add(BuildPermissionButton("계획 중심", "#EEF2FF", "#C7D2FE", "#3730A3", PermissionModeCatalog.Plan)); + actions.Children.Add(BuildPermissionButton("완전 자동", "#FFF7ED", "#FDBA74", "#C2410C", PermissionModeCatalog.BypassPermissions)); + actions.Children.Add(BuildPermissionButton("질문 없이 진행", "#FEE2E2", "#FCA5A5", "#B91C1C", PermissionModeCatalog.DontAsk)); actions.Children.Add(BuildPermissionButton("해제", "#F3F4F6", "#D1D5DB", "#374151", null, margin: false)); return actions; }