diff --git a/README.md b/README.md index c95f90a..879dd73 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저 개발 참고: Claw Code 동등성 작업 추적 문서 `docs/claw-code-parity-plan.md` +- 업데이트: 2026-04-06 09:14 (KST) +- AX Agent 워크트리 선택 팝업과 공통 선택 row 렌더를 `ChatWindow.SelectionPopupPresentation.cs`로 분리했습니다. 작업 위치/워크트리 전환 메뉴와 선택 상태 row 조립이 메인 창 코드 밖으로 이동해 footer 선택 UX를 별도 파일에서 정리할 수 있게 됐습니다. +- `ChatWindow.xaml.cs`는 대화 상태와 세션 orchestration 쪽에 더 집중하도록 정리했고, 향후 브랜치/워크트리/선택형 팝업 UX를 `claw-code` 기준으로 계속 다듬기 쉬운 구조를 만들었습니다. + - 업데이트: 2026-04-06 09:03 (KST) - AX Agent 공통 선택 팝업 조립 로직을 `ChatWindow.PopupPresentation.cs`로 분리했습니다. 테마 팝업 컨테이너, 공통 메뉴 아이템, 구분선, 최근 폴더 우클릭 컨텍스트 메뉴가 메인 창 코드 밖으로 이동해 footer/file-browser 쪽 팝업 품질 작업을 이어가기 쉬운 구조로 정리했습니다. - `ChatWindow.xaml.cs`는 대화 상태와 런타임 orchestration 쪽에 더 집중하도록 정리했고, 공통 팝업 시각 언어를 한 곳에서 다듬을 수 있는 기반을 만들었습니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 984273e..4eebdc9 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,5 +1,8 @@ # AX Copilot - 媛쒕컻 臾몄꽌 +- Document update: 2026-04-06 09:14 (KST) - Split worktree-selection popup rendering into `ChatWindow.SelectionPopupPresentation.cs`. The current workspace/worktree chooser and the shared selected-row popup card renderer now live outside `ChatWindow.xaml.cs`, reducing footer selection UI density in the main window file. +- Document update: 2026-04-06 09:14 (KST) - This keeps branch/worktree/footer chooser UX on the same presentation side of the codebase and leaves the main chat window more focused on runtime orchestration and conversation flow. + - Document update: 2026-04-06 09:03 (KST) - Split common themed popup construction out of `ChatWindow.xaml.cs` into `ChatWindow.PopupPresentation.cs`. Shared popup container creation, generic popup menu items/separators, and the recent-folder context menu now live in a dedicated partial instead of the main window orchestration file. - Document update: 2026-04-06 09:03 (KST) - This keeps footer/file-browser popup styling on a single visual path and reduces direct popup composition inside the main chat window flow, making further `claw-code` style popup UX work easier to maintain. diff --git a/src/AxCopilot/Views/ChatWindow.SelectionPopupPresentation.cs b/src/AxCopilot/Views/ChatWindow.SelectionPopupPresentation.cs new file mode 100644 index 0000000..f560c73 --- /dev/null +++ b/src/AxCopilot/Views/ChatWindow.SelectionPopupPresentation.cs @@ -0,0 +1,184 @@ +using System; +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; +using AxCopilot.Services; +using AxCopilot.Services.Agent; + +namespace AxCopilot.Views; + +public partial class ChatWindow +{ + private void ShowWorktreeMenu(UIElement placementTarget) + { + var (popup, panel) = CreateThemedPopupMenu(placementTarget, PlacementMode.Top, 320); + var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; + var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; + var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; + var currentFolder = GetCurrentWorkFolder(); + var root = string.IsNullOrWhiteSpace(currentFolder) ? "" : WorktreeStateStore.ResolveRoot(currentFolder); + var active = string.IsNullOrWhiteSpace(root) ? currentFolder : WorktreeStateStore.Load(root).Active; + var variants = GetAvailableWorkspaceVariants(root, active); + + panel.Children.Add(CreatePopupSummaryStrip(new[] + { + ("모드", string.Equals(active, root, StringComparison.OrdinalIgnoreCase) ? "로컬" : "워크트리", "#F8FAFC", "#E2E8F0", "#475569"), + ("변형", variants.Count.ToString(), "#EFF6FF", "#BFDBFE", "#1D4ED8"), + })); + panel.Children.Add(CreatePopupSectionLabel("현재 작업 위치", new Thickness(8, 6, 8, 4))); + + panel.Children.Add(CreatePopupMenuRow( + "\uED25", + "로컬", + string.IsNullOrWhiteSpace(root) ? "현재 워크스페이스" : root, + !string.IsNullOrWhiteSpace(root) && string.Equals(active, root, StringComparison.OrdinalIgnoreCase), + accentBrush, + secondaryText, + primaryText, + () => + { + popup.IsOpen = false; + if (!string.IsNullOrWhiteSpace(root)) + SwitchToWorkspace(root, root); + })); + + if (!string.IsNullOrWhiteSpace(active) && !string.Equals(active, root, StringComparison.OrdinalIgnoreCase)) + { + panel.Children.Add(CreatePopupMenuRow( + "\uE7BA", + Path.GetFileName(active), + active, + true, + accentBrush, + secondaryText, + primaryText, + () => + { + popup.IsOpen = false; + SwitchToWorkspace(active, root); + })); + } + + if (variants.Count > 0) + { + panel.Children.Add(CreatePopupSectionLabel($"워크트리 / 복사본 · {variants.Count}", new Thickness(8, 10, 8, 4))); + foreach (var variant in variants) + { + var isActive = !string.IsNullOrWhiteSpace(active) && + string.Equals(Path.GetFullPath(variant), Path.GetFullPath(active), StringComparison.OrdinalIgnoreCase); + panel.Children.Add(CreatePopupMenuRow( + "\uE8B7", + Path.GetFileName(variant), + isActive ? $"현재 선택 · {variant}" : variant, + isActive, + accentBrush, + secondaryText, + primaryText, + () => + { + popup.IsOpen = false; + SwitchToWorkspace(variant, root); + })); + } + } + + panel.Children.Add(CreatePopupSectionLabel("새 작업 위치", new Thickness(8, 10, 8, 4))); + panel.Children.Add(CreatePopupMenuRow( + "\uE943", + "현재 브랜치로 워크트리 생성", + "Git 저장소면 분리된 작업 복사본을 만들고 전환합니다", + false, + accentBrush, + secondaryText, + primaryText, + () => + { + popup.IsOpen = false; + _ = CreateCurrentBranchWorktreeAsync(); + })); + + popup.IsOpen = true; + } + + private Border CreatePopupMenuRow( + string icon, + string title, + string description, + bool selected, + Brush accentBrush, + Brush secondaryText, + Brush primaryText, + Action? onClick) + { + var hintBackground = TryFindResource("HintBackground") as Brush ?? BrushFromHex("#F8FAFC"); + var hoverBackground = TryFindResource("ItemHoverBackground") as Brush ?? BrushFromHex("#F8FAFC"); + var row = new Border + { + Background = selected ? hintBackground : Brushes.Transparent, + BorderBrush = selected ? BrushFromHex("#D6E4FF") : Brushes.Transparent, + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(12), + Padding = new Thickness(12, 10, 12, 10), + Cursor = Cursors.Hand, + Margin = new Thickness(0, 0, 0, 6), + }; + + var grid = new Grid(); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var iconBlock = new TextBlock + { + Text = icon, + FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontSize = 14, + Foreground = selected ? accentBrush : secondaryText, + Margin = new Thickness(0, 1, 10, 0), + VerticalAlignment = VerticalAlignment.Center, + }; + grid.Children.Add(iconBlock); + + var textStack = new StackPanel(); + textStack.Children.Add(new TextBlock + { + Text = title, + FontSize = 13.5, + FontWeight = selected ? FontWeights.SemiBold : FontWeights.Medium, + Foreground = primaryText, + }); + if (!string.IsNullOrWhiteSpace(description)) + { + textStack.Children.Add(new TextBlock + { + Text = description, + FontSize = 11.5, + Foreground = secondaryText, + Margin = new Thickness(0, 3, 0, 0), + TextWrapping = TextWrapping.Wrap, + }); + } + + Grid.SetColumn(textStack, 1); + grid.Children.Add(textStack); + + if (selected) + { + var check = CreateSimpleCheck(accentBrush, 14); + Grid.SetColumn(check, 2); + check.Margin = new Thickness(10, 0, 0, 0); + if (check is FrameworkElement element) + element.VerticalAlignment = VerticalAlignment.Center; + grid.Children.Add(check); + } + + row.Child = grid; + row.MouseEnter += (_, _) => row.Background = selected ? hintBackground : hoverBackground; + row.MouseLeave += (_, _) => row.Background = selected ? hintBackground : Brushes.Transparent; + row.MouseLeftButtonUp += (_, _) => onClick?.Invoke(); + return row; + } +} diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 66ae540..8960b7b 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -10700,176 +10700,6 @@ public partial class ChatWindow : Window .ToList(); } - private void ShowWorktreeMenu(UIElement placementTarget) - { - var (popup, panel) = CreateThemedPopupMenu(placementTarget, PlacementMode.Top, 320); - var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; - var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; - var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; - var currentFolder = GetCurrentWorkFolder(); - var root = string.IsNullOrWhiteSpace(currentFolder) ? "" : WorktreeStateStore.ResolveRoot(currentFolder); - var active = string.IsNullOrWhiteSpace(root) ? currentFolder : WorktreeStateStore.Load(root).Active; - var variants = GetAvailableWorkspaceVariants(root, active); - - panel.Children.Add(CreatePopupSummaryStrip(new[] - { - ("모드", string.Equals(active, root, StringComparison.OrdinalIgnoreCase) ? "로컬" : "워크트리", "#F8FAFC", "#E2E8F0", "#475569"), - ("변형", variants.Count.ToString(), "#EFF6FF", "#BFDBFE", "#1D4ED8"), - })); - panel.Children.Add(CreatePopupSectionLabel("현재 작업 위치", new Thickness(8, 6, 8, 4))); - - panel.Children.Add(CreatePopupMenuRow( - "\uED25", - "로컬", - string.IsNullOrWhiteSpace(root) ? "현재 워크스페이스" : root, - !string.IsNullOrWhiteSpace(root) && string.Equals(active, root, StringComparison.OrdinalIgnoreCase), - accentBrush, - secondaryText, - primaryText, - () => - { - popup.IsOpen = false; - if (!string.IsNullOrWhiteSpace(root)) - SwitchToWorkspace(root, root); - })); - - if (!string.IsNullOrWhiteSpace(active) && !string.Equals(active, root, StringComparison.OrdinalIgnoreCase)) - { - panel.Children.Add(CreatePopupMenuRow( - "\uE7BA", - Path.GetFileName(active), - active, - true, - accentBrush, - secondaryText, - primaryText, - () => - { - popup.IsOpen = false; - SwitchToWorkspace(active, root); - })); - } - - if (variants.Count > 0) - { - panel.Children.Add(CreatePopupSectionLabel($"워크트리 / 복사본 · {variants.Count}", new Thickness(8, 10, 8, 4))); - foreach (var variant in variants) - { - var isActive = !string.IsNullOrWhiteSpace(active) && - string.Equals(Path.GetFullPath(variant), Path.GetFullPath(active), StringComparison.OrdinalIgnoreCase); - panel.Children.Add(CreatePopupMenuRow( - "\uE8B7", - Path.GetFileName(variant), - isActive ? $"현재 선택 · {variant}" : variant, - isActive, - accentBrush, - secondaryText, - primaryText, - () => - { - popup.IsOpen = false; - SwitchToWorkspace(variant, root); - })); - } - } - - panel.Children.Add(CreatePopupSectionLabel("새 작업 위치", new Thickness(8, 10, 8, 4))); - - panel.Children.Add(CreatePopupMenuRow( - "\uE943", - "현재 브랜치로 워크트리 생성", - "Git 저장소면 분리된 작업 복사본을 만들고 전환합니다", - false, - accentBrush, - secondaryText, - primaryText, - () => - { - popup.IsOpen = false; - _ = CreateCurrentBranchWorktreeAsync(); - })); - - popup.IsOpen = true; - } - - private Border CreatePopupMenuRow( - string icon, - string title, - string description, - bool selected, - Brush accentBrush, - Brush secondaryText, - Brush primaryText, - Action? onClick) - { - var hintBackground = TryFindResource("HintBackground") as Brush ?? BrushFromHex("#F8FAFC"); - var hoverBackground = TryFindResource("ItemHoverBackground") as Brush ?? BrushFromHex("#F8FAFC"); - var row = new Border - { - Background = selected ? hintBackground : Brushes.Transparent, - BorderBrush = selected ? BrushFromHex("#D6E4FF") : Brushes.Transparent, - BorderThickness = new Thickness(1), - CornerRadius = new CornerRadius(12), - Padding = new Thickness(12, 10, 12, 10), - Cursor = Cursors.Hand, - Margin = new Thickness(0, 0, 0, 6), - }; - - var grid = new Grid(); - grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); - grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); - grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); - - var iconBlock = new TextBlock - { - Text = icon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), - FontSize = 14, - Foreground = selected ? accentBrush : secondaryText, - Margin = new Thickness(0, 1, 10, 0), - VerticalAlignment = VerticalAlignment.Center, - }; - grid.Children.Add(iconBlock); - - var textStack = new StackPanel(); - textStack.Children.Add(new TextBlock - { - Text = title, - FontSize = 13.5, - FontWeight = selected ? FontWeights.SemiBold : FontWeights.Medium, - Foreground = primaryText, - }); - if (!string.IsNullOrWhiteSpace(description)) - { - textStack.Children.Add(new TextBlock - { - Text = description, - FontSize = 11.5, - Foreground = secondaryText, - Margin = new Thickness(0, 3, 0, 0), - TextWrapping = TextWrapping.Wrap, - }); - } - Grid.SetColumn(textStack, 1); - grid.Children.Add(textStack); - - if (selected) - { - var check = CreateSimpleCheck(accentBrush, 14); - Grid.SetColumn(check, 2); - check.Margin = new Thickness(10, 0, 0, 0); - if (check is FrameworkElement element) - element.VerticalAlignment = VerticalAlignment.Center; - grid.Children.Add(check); - } - - row.Child = grid; - row.MouseEnter += (_, _) => row.Background = selected ? hintBackground : hoverBackground; - row.MouseLeave += (_, _) => row.Background = selected ? hintBackground : Brushes.Transparent; - row.MouseLeftButtonUp += (_, _) => onClick?.Invoke(); - return row; - } - private void SwitchToWorkspace(string targetPath, string rootPath) { if (string.IsNullOrWhiteSpace(targetPath) || !Directory.Exists(targetPath))