슬래시 팝업 UX 개선: 스크롤/선택 이동 재렌더링 제거
Some checks failed
Release Gate / gate (push) Has been cancelled

- ChatWindow slash 이동 경로를 RenderSlashPage 기반에서 선택 하이라이트 갱신 기반으로 전환

- 휠/방향키/Home/End 이동 시 UpdateSlashSelectionVisualState + EnsureSlashSelectionVisible 호출로 통일

- 항목 hover 시에도 동일 선택 상태 동기화 경로를 사용해 체감 일관성 개선

- README.md, docs/DEVELOPMENT.md에 2026-04-04 14:01(KST) 기준 이력 반영

- 검증: dotnet build 0경고/0오류, 관련 필터 테스트 84건 통과
This commit is contained in:
2026-04-04 14:02:30 +09:00
parent 0bb37d9390
commit 157332df52
3 changed files with 52 additions and 14 deletions

View File

@@ -222,7 +222,7 @@ public class MyHandler : IActionHandler
### v0.7.3 — AX Agent 권한 코어 재구성 + 입력 계층 정리 ### v0.7.3 — AX Agent 권한 코어 재구성 + 입력 계층 정리
업데이트: 2026-04-04 13:55 (KST) 업데이트: 2026-04-04 14:01 (KST)
| 분류 | 내용 | | 분류 | 내용 |
|------|------| |------|------|
@@ -281,6 +281,7 @@ public class MyHandler : IActionHandler
| 탭별 설정 해석기 도입 | `AgentTabSettingsResolver`를 추가해 Cowork/Code 분기(검증 활성/Code 전용 도구 비활성)를 단일 경로로 정리 | | 탭별 설정 해석기 도입 | `AgentTabSettingsResolver`를 추가해 Cowork/Code 분기(검증 활성/Code 전용 도구 비활성)를 단일 경로로 정리 |
| L4 통합 회귀 보강 | `PermissionModeCatalogTests`/`PermissionModePresentationCatalogTests`/`SlashCommandCatalogTests`를 추가하고 deny 우선 규칙을 `OperationModePolicyTests`에 반영해 권한·슬래시 회귀망을 강화 | | L4 통합 회귀 보강 | `PermissionModeCatalogTests`/`PermissionModePresentationCatalogTests`/`SlashCommandCatalogTests`를 추가하고 deny 우선 규칙을 `OperationModePolicyTests`에 반영해 권한·슬래시 회귀망을 강화 |
| 권한 팝업 핵심 4모드 정렬 | 권한 팝업을 `소극 활용/적극 활용/계획 중심/완전 자동` 중심으로 단순화하고 `활용하지 않음/질문 없이 진행``고급 모드` 접힘 섹션으로 분리 | | 권한 팝업 핵심 4모드 정렬 | 권한 팝업을 `소극 활용/적극 활용/계획 중심/완전 자동` 중심으로 단순화하고 `활용하지 않음/질문 없이 진행``고급 모드` 접힘 섹션으로 분리 |
| slash 스크롤 체감 개선 | 휠/방향키 이동 시 전체 재렌더링을 제거하고 선택 하이라이트만 갱신하도록 바꿔 `/` 팝업 스크롤 반응성과 안정성을 개선 |
| 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 |

View File

@@ -3413,3 +3413,20 @@ else:
- `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).
- 테스트 재빌드 시 WPF 임시 생성물 누락(CS2001, `AxCopilot_*_wpftmp.csproj`)이 간헐 발생하여 다음으로 회귀 확인: - 테스트 재빌드 시 WPF 임시 생성물 누락(CS2001, `AxCopilot_*_wpftmp.csproj`)이 간헐 발생하여 다음으로 회귀 확인:
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj --no-build --filter "FullyQualifiedName~ChatWindowSlashPolicyTests|FullyQualifiedName~OperationModePolicyTests|FullyQualifiedName~PermissionModeCatalogTests|FullyQualifiedName~PermissionModePresentationCatalogTests|FullyQualifiedName~OperationModeReadinessTests"` 통과 (86 passed, 0 failed). - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj --no-build --filter "FullyQualifiedName~ChatWindowSlashPolicyTests|FullyQualifiedName~OperationModePolicyTests|FullyQualifiedName~PermissionModeCatalogTests|FullyQualifiedName~PermissionModePresentationCatalogTests|FullyQualifiedName~OperationModeReadinessTests"` 통과 (86 passed, 0 failed).
## 2026-04-04 추가 진행 기록 (연속 실행 31차: slash 팝업 스크롤/선택 체감 개선)
업데이트: 2026-04-04 14:01 (KST)
### 1) slash 이동 시 재렌더링 제거
- 기존: 휠/방향키 이동마다 `RenderSlashPage()`를 호출해 전체 항목을 다시 그림.
- 변경: 이동 시 `UpdateSlashSelectionVisualState()` + `EnsureSlashSelectionVisible()`만 호출하도록 전환.
- 효과: 스크롤 시 깜빡임/점프를 줄이고 선택 이동 체감이 더 부드러워짐.
### 2) 선택 하이라이트 동기화 경로 정리
- 항목 Hover/Home/End 이동에서 동일한 하이라이트 갱신 경로를 사용하도록 통일.
- 목록 재렌더가 필요한 경우(그룹 접힘/펼침, 검색 결과 변경)만 `RenderSlashPage()` 유지.
### 3) 품질 게이트
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Debug -p:UseSharedCompilation=false -nodeReuse:false` 통과 (경고 0, 오류 0).
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj --no-build --filter "FullyQualifiedName~ChatWindowSlashPolicyTests|FullyQualifiedName~SlashCommandCatalogTests|FullyQualifiedName~OperationModePolicyTests|FullyQualifiedName~PermissionModeCatalogTests|FullyQualifiedName~PermissionModePresentationCatalogTests"` 통과 (84 passed, 0 failed).

View File

@@ -5315,7 +5315,6 @@ public partial class ChatWindow : Window
var skillAvailable = skillDef?.IsAvailable ?? true; var skillAvailable = skillDef?.IsAvailable ?? true;
var absoluteIndex = i; var absoluteIndex = i;
var isSelected = absoluteIndex == _slashPalette.SelectedIndex;
var hoverBrushItem = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.LightGray; var hoverBrushItem = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.LightGray;
var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent; var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent;
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
@@ -5325,8 +5324,8 @@ public partial class ChatWindow : Window
var item = new Border var item = new Border
{ {
Background = isSelected ? hoverBrushItem : itemBg, Background = Brushes.Transparent,
BorderBrush = isSelected ? accent : borderBrush, BorderBrush = borderBrush,
BorderThickness = new Thickness(1), BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(10), CornerRadius = new CornerRadius(10),
Padding = new Thickness(10, 7, 10, 7), Padding = new Thickness(10, 7, 10, 7),
@@ -5440,14 +5439,9 @@ public partial class ChatWindow : Window
item.MouseEnter += (_, _) => item.MouseEnter += (_, _) =>
{ {
_slashPalette.SelectedIndex = absoluteIndex; _slashPalette.SelectedIndex = absoluteIndex;
item.Background = hoverBrushItem; UpdateSlashSelectionVisualState();
item.BorderBrush = accent;
};
item.MouseLeave += (_, _) =>
{
item.Background = absoluteIndex == _slashPalette.SelectedIndex ? hoverBrushItem : itemBg;
item.BorderBrush = absoluteIndex == _slashPalette.SelectedIndex ? accent : borderBrush;
}; };
item.MouseLeave += (_, _) => UpdateSlashSelectionVisualState();
item.MouseLeftButtonDown += (_, _) => item.MouseLeftButtonDown += (_, _) =>
{ {
_slashPalette.SelectedIndex = absoluteIndex; _slashPalette.SelectedIndex = absoluteIndex;
@@ -5499,6 +5493,7 @@ public partial class ChatWindow : Window
SlashPopupFooter.Text = $"Enter 실행 · ↑↓/PgUp/PgDn 이동 · Home/End · Esc 닫기 · 표시 {visibleCommandCount + visibleSkillCount}/{total}"; SlashPopupFooter.Text = $"Enter 실행 · ↑↓/PgUp/PgDn 이동 · Home/End · Esc 닫기 · 표시 {visibleCommandCount + visibleSkillCount}/{total}";
} }
UpdateSlashSelectionVisualState();
EnsureSlashSelectionVisible(); EnsureSlashSelectionVisible();
} }
@@ -5546,7 +5541,8 @@ public partial class ChatWindow : Window
for (var i = 0; i < steps; i++) for (var i = 0; i < steps; i++)
MoveSlashSelection(direction); MoveSlashSelection(direction);
RenderSlashPage(); UpdateSlashSelectionVisualState();
EnsureSlashSelectionVisible();
} }
/// <summary>키보드로 선택된 슬래시 아이템을 실행합니다.</summary> /// <summary>키보드로 선택된 슬래시 아이템을 실행합니다.</summary>
@@ -5591,6 +5587,28 @@ public partial class ChatWindow : Window
SlashScrollViewer.ScrollToVerticalOffset(SlashScrollViewer.VerticalOffset + (bounds.Bottom - SlashScrollViewer.ViewportHeight) + 8); SlashScrollViewer.ScrollToVerticalOffset(SlashScrollViewer.VerticalOffset + (bounds.Bottom - SlashScrollViewer.ViewportHeight) + 8);
} }
private void UpdateSlashSelectionVisualState()
{
if (_slashVisibleItemByAbsoluteIndex.Count == 0)
return;
var selectedIndex = _slashPalette.SelectedIndex;
var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent;
var hoverBrushItem = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.LightGray;
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
foreach (var (absoluteIndex, element) in _slashVisibleItemByAbsoluteIndex)
{
if (element is not Border border)
continue;
var selected = absoluteIndex == selectedIndex;
border.Background = selected ? hoverBrushItem : itemBg;
border.BorderBrush = selected ? accent : borderBrush;
}
}
/// <summary>슬래시 명령어 즐겨찾기를 토글하고 설정을 저장합니다.</summary> /// <summary>슬래시 명령어 즐겨찾기를 토글하고 설정을 저장합니다.</summary>
private void ToggleSlashFavorite(string cmd) private void ToggleSlashFavorite(string cmd)
{ {
@@ -10881,14 +10899,16 @@ public partial class ChatWindow : Window
{ {
var visible = GetVisibleSlashOrderedIndices(); var visible = GetVisibleSlashOrderedIndices();
_slashPalette.SelectedIndex = visible.Count > 0 ? visible[0] : GetFirstVisibleSlashIndex(_slashPalette.Matches); _slashPalette.SelectedIndex = visible.Count > 0 ? visible[0] : GetFirstVisibleSlashIndex(_slashPalette.Matches);
RenderSlashPage(); UpdateSlashSelectionVisualState();
EnsureSlashSelectionVisible();
e.Handled = true; e.Handled = true;
} }
else if (e.Key == Key.End) else if (e.Key == Key.End)
{ {
var visible = GetVisibleSlashOrderedIndices(); var visible = GetVisibleSlashOrderedIndices();
_slashPalette.SelectedIndex = visible.Count > 0 ? visible[^1] : GetFirstVisibleSlashIndex(_slashPalette.Matches); _slashPalette.SelectedIndex = visible.Count > 0 ? visible[^1] : GetFirstVisibleSlashIndex(_slashPalette.Matches);
RenderSlashPage(); UpdateSlashSelectionVisualState();
EnsureSlashSelectionVisible();
e.Handled = true; e.Handled = true;
} }
else if (e.Key == Key.Enter && _slashPalette.SelectedIndex >= 0) else if (e.Key == Key.Enter && _slashPalette.SelectedIndex >= 0)