권한 순환/슬래시 스크롤 체감 보강: claw-code 기준 코어 모드 정렬
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.NextPermission 순환을 Deny->Default->AcceptEdits->Plan->BypassPermissions->Deny로 정렬하고 DontAsk는 기본 순환에서 제외\n- AgentSettingsWindow 권한 모드 버튼 순환도 동일 코어 순환으로 맞춰 인라인/설정 체계를 일관화\n- SlashPopup_ScrollByDelta 개선: ScrollViewer 오프셋 이동 + 뷰포트 상단 기준 선택 동기화로 휠 스크롤 체감 개선\n- ChatWindowSlashPolicyTests에 NextPermission 순환 회귀 테스트 추가(Bypass/DontAsk 포함)\n- README.md, docs/DEVELOPMENT.md에 2026-04-04 17:27(KST) 이력 및 검증 결과 기록\n- 검증: dotnet build 경고0/오류0, dotnet test 필터 65 passed
This commit is contained in:
@@ -295,6 +295,7 @@ public class MyHandler : IActionHandler
|
|||||||
| 슬래시 네비게이션 입력 보강 | InputBox 포커스 상태에서도 방향키/Page/Home/End/Tab이 슬래시 목록 탐색에 즉시 반영되도록 키 처리 경로를 통합하고, 모든 그룹 접힘 상태에서 휠 스크롤 fallback을 추가 |
|
| 슬래시 네비게이션 입력 보강 | InputBox 포커스 상태에서도 방향키/Page/Home/End/Tab이 슬래시 목록 탐색에 즉시 반영되도록 키 처리 경로를 통합하고, 모든 그룹 접힘 상태에서 휠 스크롤 fallback을 추가 |
|
||||||
| 사내/사외 모드 회귀 보강 | operationMode 전환 직후 WebSearch 동작 반영과 URL 판별 경계(HTTP/파일/mailto) 테스트를 추가해 내부 차단 정책의 즉시성/정확성을 강화 |
|
| 사내/사외 모드 회귀 보강 | operationMode 전환 직후 WebSearch 동작 반영과 URL 판별 경계(HTTP/파일/mailto) 테스트를 추가해 내부 차단 정책의 즉시성/정확성을 강화 |
|
||||||
| AX Agent 설정창 오픈 안정화 | `ToggleSwitch`를 전역 리소스로 승격해 AX Agent 창 초기화 시 리소스 누락 예외를 방지하고, AX Agent 설정창에는 테마 사전만 안전 주입하도록 오픈 경로를 보강 |
|
| AX Agent 설정창 오픈 안정화 | `ToggleSwitch`를 전역 리소스로 승격해 AX Agent 창 초기화 시 리소스 누락 예외를 방지하고, AX Agent 설정창에는 테마 사전만 안전 주입하도록 오픈 경로를 보강 |
|
||||||
|
| 권한 순환/슬래시 스크롤 체감 보강 | `claw-code` 기준으로 권한 순환에서 고위험 `질문 없이 진행`을 기본 순환에서 분리하고, `/` 팝업 휠 스크롤 시 뷰포트 기준 선택 동기화를 추가해 스크롤 사용성을 개선 |
|
||||||
| Slash palette 상태 분리 시작 | `ChatWindow`에 몰려 있던 slash 상태를 `SlashPaletteState`로 분리해 이후 Codex/Claude형 composer 개편 기반 마련 |
|
| Slash palette 상태 분리 시작 | `ChatWindow`에 몰려 있던 slash 상태를 `SlashPaletteState`로 분리해 이후 Codex/Claude형 composer 개편 기반 마련 |
|
||||||
| 런처 이미지 미리보기 추가 | `#` 클립보드 이미지 항목에서 `Shift+Enter`로 전용 미리보기 창을 열고, 줌·원본 해상도 확인·PNG/JPEG/BMP 저장·클립보드 복사를 지원 |
|
| 런처 이미지 미리보기 추가 | `#` 클립보드 이미지 항목에서 `Shift+Enter`로 전용 미리보기 창을 열고, 줌·원본 해상도 확인·PNG/JPEG/BMP 저장·클립보드 복사를 지원 |
|
||||||
| 검증 | `dotnet build` 경고 0 / 오류 0, `dotnet test` 436 passed / 0 failed |
|
| 검증 | `dotnet build` 경고 0 / 오류 0, `dotnet test` 436 passed / 0 failed |
|
||||||
|
|||||||
@@ -3712,3 +3712,33 @@ else:
|
|||||||
### 3) 품질 게이트
|
### 3) 품질 게이트
|
||||||
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Debug -p:UseSharedCompilation=false -nodeReuse:false` 통과 (경고 0, 오류 0).
|
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Debug -p:UseSharedCompilation=false -nodeReuse:false` 통과 (경고 0, 오류 0).
|
||||||
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Debug --filter "ChatWindowSlashPolicyTests|OperationModePolicyTests|OperationModeReadinessTests"` 통과 (59 passed, 0 failed).
|
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Debug --filter "ChatWindowSlashPolicyTests|OperationModePolicyTests|OperationModeReadinessTests"` 통과 (59 passed, 0 failed).
|
||||||
|
|
||||||
|
## 2026-04-04 추가 진행 기록 (연속 실행 45차: 권한 순환/슬래시 스크롤 체감 보강)
|
||||||
|
|
||||||
|
업데이트: 2026-04-04 17:27 (KST)
|
||||||
|
|
||||||
|
### 1) 권한 모드 기본 순환 정렬
|
||||||
|
- `claw-code`의 코어 모드 순환 기준에 맞춰 기본 순환을 `Deny → Default → AcceptEdits → Plan → BypassPermissions → Deny`로 고정.
|
||||||
|
- `DontAsk`는 고위험 모드로 분리해 기본 순환(인라인/슬래시/설정창 버튼)에서 제외하고 명시 선택으로만 진입하도록 조정.
|
||||||
|
- 반영 위치:
|
||||||
|
- `ChatWindow.NextPermission(...)`
|
||||||
|
- `AgentSettingsWindow.BtnPermissionMode_Click(...)`
|
||||||
|
|
||||||
|
### 2) 슬래시 팝업 휠 스크롤 반응 보강
|
||||||
|
- `/` 팝업 휠 입력 시:
|
||||||
|
1. `ScrollViewer` 오프셋을 실제로 이동
|
||||||
|
2. 뷰포트 상단에 가장 가까운 항목으로 선택 인덱스를 동기화
|
||||||
|
3. 선택 강조/가시화 갱신
|
||||||
|
- 효과:
|
||||||
|
- 터치패드/마우스에서 스크롤 체감이 더 자연스러워지고, 선택 하이라이트와 실제 스크롤 위치 불일치가 완화됨.
|
||||||
|
|
||||||
|
### 3) 회귀 테스트 추가
|
||||||
|
- `ChatWindowSlashPolicyTests.NextPermission_ShouldCycleCoreModesAndReturnToDeny` 추가.
|
||||||
|
- 검증 케이스:
|
||||||
|
- `BypassPermissions -> Deny`
|
||||||
|
- `DontAsk -> Deny`
|
||||||
|
- 코어 순환 전 구간.
|
||||||
|
|
||||||
|
### 4) 품질 게이트
|
||||||
|
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Debug -p:UseSharedCompilation=false -nodeReuse:false` 통과 (경고 0, 오류 0).
|
||||||
|
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Debug -p:UseSharedCompilation=false -nodeReuse:false --filter "ChatWindowSlashPolicyTests|OperationModePolicyTests|OperationModeReadinessTests"` 통과 (65 passed, 0 failed).
|
||||||
|
|||||||
@@ -1,11 +1,28 @@
|
|||||||
using AxCopilot.Views;
|
using AxCopilot.Views;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
namespace AxCopilot.Tests.Views;
|
namespace AxCopilot.Tests.Views;
|
||||||
|
|
||||||
public class ChatWindowSlashPolicyTests
|
public class ChatWindowSlashPolicyTests
|
||||||
{
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Deny", "Default")]
|
||||||
|
[InlineData("Default", "AcceptEdits")]
|
||||||
|
[InlineData("AcceptEdits", "Plan")]
|
||||||
|
[InlineData("Plan", "BypassPermissions")]
|
||||||
|
[InlineData("BypassPermissions", "Deny")]
|
||||||
|
[InlineData("DontAsk", "Deny")]
|
||||||
|
public void NextPermission_ShouldCycleCoreModesAndReturnToDeny(string current, string expected)
|
||||||
|
{
|
||||||
|
var method = typeof(ChatWindow).GetMethod("NextPermission", BindingFlags.NonPublic | BindingFlags.Static);
|
||||||
|
method.Should().NotBeNull();
|
||||||
|
|
||||||
|
var result = method!.Invoke(null, new object[] { current }) as string;
|
||||||
|
result.Should().Be(expected);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void BuildVerifySystemPrompt_ShouldContainStructuredSections()
|
public void BuildVerifySystemPrompt_ShouldContainStructuredSections()
|
||||||
{
|
{
|
||||||
@@ -40,9 +57,12 @@ public class ChatWindowSlashPolicyTests
|
|||||||
[InlineData("model", "model", "")]
|
[InlineData("model", "model", "")]
|
||||||
[InlineData("ask", "ask", "")]
|
[InlineData("ask", "ask", "")]
|
||||||
[InlineData("reconnect all", "reconnect", "all")]
|
[InlineData("reconnect all", "reconnect", "all")]
|
||||||
|
[InlineData("/compact", "open", "")]
|
||||||
|
[InlineData("compact now", "compact", "now")]
|
||||||
public void ParseGenericAction_ShouldParseExpected(string displayText, string expectedAction, string expectedArgument)
|
public void ParseGenericAction_ShouldParseExpected(string displayText, string expectedAction, string expectedArgument)
|
||||||
{
|
{
|
||||||
var (action, argument) = ChatWindow.ParseGenericAction(displayText, "/settings");
|
var command = displayText.StartsWith("/compact", StringComparison.OrdinalIgnoreCase) ? "/compact" : "/settings";
|
||||||
|
var (action, argument) = ChatWindow.ParseGenericAction(displayText, command);
|
||||||
|
|
||||||
action.Should().Be(expectedAction);
|
action.Should().Be(expectedAction);
|
||||||
argument.Should().Be(expectedArgument);
|
argument.Should().Be(expectedArgument);
|
||||||
@@ -60,6 +80,19 @@ public class ChatWindowSlashPolicyTests
|
|||||||
argument.Should().Be(expectedArgument);
|
argument.Should().Be(expectedArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/status", "open", "")]
|
||||||
|
[InlineData("test", "test", "")]
|
||||||
|
[InlineData("connection", "connection", "")]
|
||||||
|
[InlineData("diag now", "diag", "now")]
|
||||||
|
public void ParseGenericAction_ForStatus_ShouldParseExpected(string displayText, string expectedAction, string expectedArgument)
|
||||||
|
{
|
||||||
|
var (action, argument) = ChatWindow.ParseGenericAction(displayText, "/status");
|
||||||
|
|
||||||
|
action.Should().Be(expectedAction);
|
||||||
|
argument.Should().Be(expectedArgument);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(false, "stdio", true, false, null, "Disabled")]
|
[InlineData(false, "stdio", true, false, null, "Disabled")]
|
||||||
[InlineData(true, "stdio", false, false, null, "Enabled")]
|
[InlineData(true, "stdio", false, false, null, "Enabled")]
|
||||||
|
|||||||
@@ -328,7 +328,9 @@ public partial class AgentSettingsWindow : Window
|
|||||||
PermissionModeCatalog.Default => PermissionModeCatalog.AcceptEdits,
|
PermissionModeCatalog.Default => PermissionModeCatalog.AcceptEdits,
|
||||||
PermissionModeCatalog.AcceptEdits => PermissionModeCatalog.Plan,
|
PermissionModeCatalog.AcceptEdits => PermissionModeCatalog.Plan,
|
||||||
PermissionModeCatalog.Plan => PermissionModeCatalog.BypassPermissions,
|
PermissionModeCatalog.Plan => PermissionModeCatalog.BypassPermissions,
|
||||||
PermissionModeCatalog.BypassPermissions => PermissionModeCatalog.DontAsk,
|
// 기본 순환에서는 코어 모드만 순환하고 dontAsk는 명시 선택으로만 진입.
|
||||||
|
PermissionModeCatalog.BypassPermissions => PermissionModeCatalog.Deny,
|
||||||
|
PermissionModeCatalog.DontAsk => PermissionModeCatalog.Deny,
|
||||||
_ => PermissionModeCatalog.Deny,
|
_ => PermissionModeCatalog.Deny,
|
||||||
};
|
};
|
||||||
RefreshModeLabels();
|
RefreshModeLabels();
|
||||||
|
|||||||
@@ -5574,6 +5574,40 @@ public partial class ChatWindow : Window
|
|||||||
_slashPalette.SelectedIndex = visibleOrder[currentPosition + 1];
|
_slashPalette.SelectedIndex = visibleOrder[currentPosition + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int? FindSlashIndexClosestToViewportTop()
|
||||||
|
{
|
||||||
|
if (SlashScrollViewer == null || _slashVisibleAbsoluteOrder.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var bestIndex = -1;
|
||||||
|
var bestDistance = double.MaxValue;
|
||||||
|
foreach (var absoluteIndex in _slashVisibleAbsoluteOrder)
|
||||||
|
{
|
||||||
|
if (!_slashVisibleItemByAbsoluteIndex.TryGetValue(absoluteIndex, out var item))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bounds = item.TransformToAncestor(SlashScrollViewer)
|
||||||
|
.TransformBounds(new Rect(0, 0, item.ActualWidth, item.ActualHeight));
|
||||||
|
|
||||||
|
// 뷰포트 상단에 가장 가까운 가시 항목을 선택 기준으로 사용.
|
||||||
|
var distance = Math.Abs(bounds.Top);
|
||||||
|
if (distance < bestDistance && bounds.Bottom >= 0)
|
||||||
|
{
|
||||||
|
bestDistance = distance;
|
||||||
|
bestIndex = absoluteIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 레이아웃 갱신 중 transform 예외는 무시.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestIndex >= 0 ? bestIndex : null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>슬래시 팝업을 Delta 방향으로 스크롤합니다.</summary>
|
/// <summary>슬래시 팝업을 Delta 방향으로 스크롤합니다.</summary>
|
||||||
private void SlashPopup_ScrollByDelta(int delta)
|
private void SlashPopup_ScrollByDelta(int delta)
|
||||||
{
|
{
|
||||||
@@ -5587,11 +5621,24 @@ public partial class ChatWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var steps = Math.Max(1, Math.Abs(delta) / 120);
|
// 터치패드/마우스 환경 모두에서 체감이 유사하도록 스크롤뷰도 함께 이동.
|
||||||
|
if (SlashScrollViewer != null)
|
||||||
|
{
|
||||||
|
var target = Math.Max(0, Math.Min(
|
||||||
|
SlashScrollViewer.ScrollableHeight,
|
||||||
|
SlashScrollViewer.VerticalOffset - (delta / 3.0)));
|
||||||
|
SlashScrollViewer.ScrollToVerticalOffset(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
var steps = Math.Max(1, (int)Math.Ceiling(Math.Abs(delta) / 120.0));
|
||||||
var direction = delta > 0 ? -1 : 1;
|
var direction = delta > 0 ? -1 : 1;
|
||||||
for (var i = 0; i < steps; i++)
|
for (var i = 0; i < steps; i++)
|
||||||
MoveSlashSelection(direction);
|
MoveSlashSelection(direction);
|
||||||
|
|
||||||
|
var viewportTopIndex = FindSlashIndexClosestToViewportTop();
|
||||||
|
if (viewportTopIndex.HasValue)
|
||||||
|
_slashPalette.SelectedIndex = viewportTopIndex.Value;
|
||||||
|
|
||||||
UpdateSlashSelectionVisualState();
|
UpdateSlashSelectionVisualState();
|
||||||
EnsureSlashSelectionVisible();
|
EnsureSlashSelectionVisible();
|
||||||
}
|
}
|
||||||
@@ -12842,7 +12889,10 @@ public partial class ChatWindow : Window
|
|||||||
"default" => "AcceptEdits",
|
"default" => "AcceptEdits",
|
||||||
"acceptedits" => "Plan",
|
"acceptedits" => "Plan",
|
||||||
"plan" => "BypassPermissions",
|
"plan" => "BypassPermissions",
|
||||||
"bypasspermissions" => "DontAsk",
|
// claw-code 기준: 기본 순환은 코어 모드까지만 이동하고,
|
||||||
|
// dontAsk는 명시 선택(고급 모드)으로만 진입.
|
||||||
|
"bypasspermissions" => "Deny",
|
||||||
|
"dontask" => "Deny",
|
||||||
_ => "Deny",
|
_ => "Deny",
|
||||||
};
|
};
|
||||||
private static string ServiceLabel(string service) => (service ?? "").ToLowerInvariant() switch
|
private static string ServiceLabel(string service) => (service ?? "").ToLowerInvariant() switch
|
||||||
|
|||||||
Reference in New Issue
Block a user