diff --git a/README.md b/README.md index c068630..d83f06c 100644 --- a/README.md +++ b/README.md @@ -222,7 +222,7 @@ public class MyHandler : IActionHandler ### v0.7.3 — AX Agent 권한 코어 재구성 + 입력 계층 정리 -업데이트: 2026-04-04 16:12 (KST) +업데이트: 2026-04-04 16:18 (KST) | 분류 | 내용 | |------|------| @@ -292,6 +292,7 @@ public class MyHandler : IActionHandler | 탭 전환 빈 대화 누적 방지 | 탭 전환 중 생성되는 무의미한 빈 대화를 저장 대상에서 제외하고, 목록에서도 빈 노이즈 항목을 숨겨 이력 누적 체감 버그를 완화 | | 권한 팝업 즉시반영 정렬 | 권한 팝업에 `활용하지 않음`을 핵심 영역 맨 위에 배치하고, 대화 권한이 없을 때도 탭 기본값(Deny/DefaultAgentPermission)을 즉시 반영하도록 로딩 경로를 보강 | | 권한 색상 체계 통일 | 권한 요약 카드/상단 배너에서 모드별 색상(Deny=녹색, Passive=파랑, Active=녹색, Plan=보라, FullAuto=주황, Silent=빨강)을 팝업 체계와 일치시킴 | +| 슬래시 네비게이션 입력 보강 | InputBox 포커스 상태에서도 방향키/Page/Home/End/Tab이 슬래시 목록 탐색에 즉시 반영되도록 키 처리 경로를 통합하고, 모든 그룹 접힘 상태에서 휠 스크롤 fallback을 추가 | | 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 346932d..7a16cab 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -3650,3 +3650,24 @@ else: ### 3) 품질 게이트 - `dotnet build src/AxCopilot/AxCopilot.csproj -c Debug -p:UseSharedCompilation=false -nodeReuse:false` 통과 (경고 0, 오류 0). - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj --filter "FullyQualifiedName~PermissionModePresentationCatalogTests|FullyQualifiedName~PermissionModeCatalogTests|FullyQualifiedName~ChatSessionStateServiceTests"` 통과 (72 passed, 0 failed). + +## 2026-04-04 추가 진행 기록 (연속 실행 42차: 슬래시 네비게이션 입력 보강) + +업데이트: 2026-04-04 16:18 (KST) + +### 1) InputBox 포커스 상태 네비게이션 보강 +- `InputBox_PreviewKeyDown` 초입에서 슬래시 전용 네비게이션 핸들러를 우선 호출하도록 변경. +- 기존에는 창 수준 `KeyDown`에 의존해 환경/포커스 상황에서 방향키가 텍스트 커서 이동으로 소비되는 케이스가 있었음. + +### 2) 슬래시 키 처리 경로 통합 +- `TryHandleSlashNavigationKey`를 신설해 `InputBox`와 창 전체 `KeyDown` 모두 같은 경로로 처리: + - `Up/Down`, `PageUp/PageDown`, `Home/End`, `Esc`, `Enter`, `Tab` +- 효과: 키보드 탐색 동작 일관성 개선. + +### 3) 휠 스크롤 fallback 추가 +- `SlashPopup_ScrollByDelta`에서 모든 그룹이 접힌 상태(가시 항목 0)일 때 `SlashScrollViewer` 오프셋을 직접 이동하도록 fallback 추가. +- 효과: 항목 선택 인덱스가 없는 상태에서도 휠 입력이 무반응처럼 보이는 현상 완화. + +### 4) 품질 게이트 +- `dotnet build src/AxCopilot/AxCopilot.csproj -c Debug -p:UseSharedCompilation=false -nodeReuse:false` 통과 (경고 0, 오류 0). +- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -p:UseSharedCompilation=false -nodeReuse:false --filter "FullyQualifiedName~ChatWindowSlashPolicyTests|FullyQualifiedName~PermissionModeCatalogTests|FullyQualifiedName~ChatSessionStateServiceTests"` 통과 (109 passed, 0 failed). diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index c94adfd..4c6006d 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -4923,6 +4923,9 @@ public partial class ChatWindow : Window private async void InputBox_PreviewKeyDown(object sender, KeyEventArgs e) { + if (TryHandleSlashNavigationKey(e)) + return; + // Ctrl+V: 클립보드 이미지 붙여넣기 if (e.Key == Key.V && Keyboard.Modifiers.HasFlag(ModifierKeys.Control)) { @@ -5571,6 +5574,13 @@ public partial class ChatWindow : Window if (_slashPalette.Matches.Count == 0) return; + if (GetVisibleSlashOrderedIndices().Count == 0) + { + if (SlashScrollViewer != null) + SlashScrollViewer.ScrollToVerticalOffset(Math.Max(0, SlashScrollViewer.VerticalOffset - delta / 3.0)); + return; + } + var steps = Math.Max(1, Math.Abs(delta) / 120); var direction = delta > 0 ? -1 : 1; for (var i = 0; i < steps; i++) @@ -10902,61 +10912,69 @@ public partial class ChatWindow : Window } // 슬래시 명령 팝업 키 처리 - if (SlashPopup.IsOpen) + if (TryHandleSlashNavigationKey(e)) + return; + + if (PermissionPopup.IsOpen && e.Key == Key.Escape) { - if (e.Key == Key.Escape) - { + PermissionPopup.IsOpen = false; + e.Handled = true; + } + } + + private bool TryHandleSlashNavigationKey(KeyEventArgs e) + { + if (!SlashPopup.IsOpen) + return false; + + switch (e.Key) + { + case Key.Escape: SlashPopup.IsOpen = false; _slashPalette.SelectedIndex = -1; e.Handled = true; - } - else if (e.Key == Key.Up) - { - SlashPopup_ScrollByDelta(120); // 위로 1칸 + return true; + case Key.Up: + SlashPopup_ScrollByDelta(120); e.Handled = true; - } - else if (e.Key == Key.Down) - { - SlashPopup_ScrollByDelta(-120); // 아래로 1칸 + return true; + case Key.Down: + SlashPopup_ScrollByDelta(-120); e.Handled = true; - } - else if (e.Key == Key.PageUp) - { + return true; + case Key.PageUp: SlashPopup_ScrollByDelta(600); e.Handled = true; - } - else if (e.Key == Key.PageDown) - { + return true; + case Key.PageDown: SlashPopup_ScrollByDelta(-600); e.Handled = true; - } - else if (e.Key == Key.Home) + return true; + case Key.Home: { var visible = GetVisibleSlashOrderedIndices(); _slashPalette.SelectedIndex = visible.Count > 0 ? visible[0] : GetFirstVisibleSlashIndex(_slashPalette.Matches); UpdateSlashSelectionVisualState(); EnsureSlashSelectionVisible(); e.Handled = true; + return true; } - else if (e.Key == Key.End) + case Key.End: { var visible = GetVisibleSlashOrderedIndices(); _slashPalette.SelectedIndex = visible.Count > 0 ? visible[^1] : GetFirstVisibleSlashIndex(_slashPalette.Matches); UpdateSlashSelectionVisualState(); EnsureSlashSelectionVisible(); e.Handled = true; + return true; } - else if (e.Key == Key.Enter && _slashPalette.SelectedIndex >= 0) - { - e.Handled = true; + case Key.Tab when _slashPalette.SelectedIndex >= 0: + case Key.Enter when _slashPalette.SelectedIndex >= 0: ExecuteSlashSelectedItem(); - } - } - - if (PermissionPopup.IsOpen && e.Key == Key.Escape) - { - PermissionPopup.IsOpen = false; - e.Handled = true; + e.Handled = true; + return true; + default: + return false; } }