슬래시 입력 UX 보강: 포커스/키 처리 통합 및 휠 fallback 추가
Some checks failed
Release Gate / gate (push) Has been cancelled

- InputBox_PreviewKeyDown에서 슬래시 네비게이션 키를 우선 처리

- TryHandleSlashNavigationKey로 창/입력창 키 경로 통합(Up/Down/Pg/Home/End/Tab/Enter/Esc)

- 가시 항목 0일 때 SlashScrollViewer 오프셋 fallback 추가

- README/DEVELOPMENT 이력(2026-04-04 16:18 KST) 동기화
This commit is contained in:
2026-04-04 16:18:44 +09:00
parent 64be0e082d
commit 3b9938e01b
3 changed files with 71 additions and 31 deletions

View File

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

View File

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

View File

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