권한 체계 표면 통일 및 AX Agent UI 단순화 연속 적용
Some checks failed
Release Gate / gate (push) Has been cancelled

- 권한 명칭을 활용하지 않음/소극 활용/적극 활용/계획 중심/완전 자동/질문 없이 진행 체계로 통일
- 권한 팝업 선택 순서를 Deny 우선 순서로 재정렬하고 고급 분리를 줄여 단일 흐름으로 정리
- 하단 권한 라벨/상단 권한 배너/작업요약 권한 액션 버튼의 용어·색상·설명을 동일 체계로 맞춤
- Chat 탭 기본 권한 적용을 활용하지 않음으로 조정하여 보수적 기본 동작 강화
- /sandbox-toggle 및 AgentSettingsWindow 권한 순환 순서를 동일 체계로 통일
- 좌측 패널 모드 배지 헤더를 숨겨 탭별 핵심 선택 중심으로 UI 밀도 단순화
- 개발문서 업데이트: README.md, docs/DEVELOPMENT.md (2026-04-04 12:22 KST)
- 운영 모드 점검 근거 반영: OperationModePolicy/Readiness/LlmOperationMode 테스트 필터 18건 통과 기록
This commit is contained in:
2026-04-04 12:23:56 +09:00
parent 57b204649e
commit de70f57277
6 changed files with 124 additions and 52 deletions

View File

@@ -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 |

View File

@@ -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).

View File

@@ -17,12 +17,12 @@ public static class PermissionModeCatalog
public static readonly IReadOnlyList<string> UserSelectableModes = new[]
{
Deny,
Default,
AcceptEdits,
Plan,
BypassPermissions,
DontAsk,
Deny,
};
/// <summary>
@@ -107,9 +107,9 @@ public static class PermissionModeCatalog
{
Default => "소극 활용",
AcceptEdits => "적극 활용",
Plan => "Plan",
BypassPermissions => "Bypass",
DontAsk => "DontAsk",
Plan => "계획 중심",
BypassPermissions => "완전 자동",
DontAsk => "질문 없이 진행",
Deny => "활용하지 않음",
_ => normalized,
};

View File

@@ -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;
}

View File

@@ -306,7 +306,7 @@
CornerRadius="10"
Padding="10,8">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
<StackPanel Orientation="Horizontal" Margin="0,0,0,4" Visibility="Collapsed">
<TextBlock x:Name="SidebarModeBadgeIcon" Text="&#xE8BD;"
FontFamily="Segoe MDL2 Assets" FontSize="12"
Foreground="{DynamicResource AccentColor}"
@@ -1477,7 +1477,7 @@
<TextBlock x:Name="PermissionIcon" Text="&#xE8D7;" FontFamily="Segoe MDL2 Assets" FontSize="12"
Foreground="{DynamicResource SecondaryText}"
VerticalAlignment="Center" Margin="0,0,4,0"/>
<TextBlock x:Name="PermissionLabel" Text="권한 요청" FontSize="12"
<TextBlock x:Name="PermissionLabel" Text="활용하지 않음" FontSize="12"
Foreground="{DynamicResource SecondaryText}"
VerticalAlignment="Center"/>
</StackPanel>

View File

@@ -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;
}