diff --git a/README.md b/README.md index e51c6ee..fbe86d2 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저 개발 참고: Claw Code 동등성 작업 추적 문서 `docs/claw-code-parity-plan.md` +- 업데이트: 2026-04-06 08:12 (KST) +- AX Agent 하단 작업 바 관련 presentation 메서드를 메인 창 코드에서 더 분리했습니다. `ChatWindow.FooterPresentation.cs`를 추가해 폴더 바, 선택된 프리셋 안내, Git 브랜치 버튼/팝업 렌더, 요약 pill 생성 책임을 별도 partial로 옮겼습니다. +- `ChatWindow.xaml.cs`는 대화 흐름과 런타임 orchestration 중심으로 더 정리했고, claw-code 기준으로 footer/preset/Git popup 품질 작업을 계속 이어가기 쉬운 구조를 만들었습니다. + - 업데이트: 2026-04-06 01:37 (KST) - AX Agent의 계획 승인 흐름을 더 transcript 우선 구조로 정리했습니다. 인라인 승인 카드가 기본 경로를 맡고, `계획` 버튼은 저장된 계획 요약/단계를 여는 상세 보기 역할만 하도록 분리했습니다. - 상태선과 quick strip 계산도 presentation state로 더 모았습니다. runtime badge, compact strip, quick strip의 텍스트/강조색/노출 여부를 한 번에 계산해 창 코드의 직접 분기를 줄였습니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index e96ac76..2b218d3 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4891,3 +4891,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - transcript display catalog를 `claw-code` 기준으로 정교화했다. [AgentTranscriptDisplayCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs)는 도구/스킬 이름과 badge label을 `파일 / 문서 / 빌드 / Git / 웹 / 질문 / 제안 / 에이전트` 축으로 재정의했고, summary fallback 문구도 더 자연스러운 한국어로 정리했다. - [PermissionRequestPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs)는 `명령 실행 / 웹 요청 / 스킬 실행 / 의견 요청 / 파일 수정 / 파일 접근` 권한 요청을 타입별 색상/라벨로 분기하게 바꿨다. [ToolResultPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs)는 도구 결과를 `파일 작업`, `빌드/테스트`, `Git`, `문서`, `스킬`, `웹 요청`, `명령 실행` 기준으로 성공/실패 라벨을 더 세밀하게 반환한다. - 이벤트 배너 renderer를 [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs) 로 분리했다. 기존 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 있던 `CreateCompactEventPill`, `AddAgentEventBanner`, `GetDecisionBadgeMeta`를 별도 partial로 옮겨, 이후 `permission/tool-result/plan` 타입별 renderer 확장을 더 쉽게 할 수 있는 구조를 마련했다. +- Document update: 2026-04-06 08:12 (KST) - Split footer/preset/Git popup presentation logic out of `ChatWindow.xaml.cs` into `ChatWindow.FooterPresentation.cs`. The folder bar refresh, selected preset guide, Git branch surface update, Git popup assembly, and popup summary-pill/row helpers now live in a dedicated partial instead of the main window orchestration file. +- Document update: 2026-04-06 08:12 (KST) - This pass keeps the AX Agent transcript/runtime flow closer to the `claw-code` separation model by reducing UI assembly inside the main chat window file and isolating footer/prompt-adjacent presentation code for future parity work. diff --git a/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs b/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs new file mode 100644 index 0000000..e9c54ee --- /dev/null +++ b/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs @@ -0,0 +1,441 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using AxCopilot.Models; + +namespace AxCopilot.Views; + +public partial class ChatWindow +{ + private void UpdateFolderBar() + { + if (FolderBar == null) return; + if (_activeTab == "Chat") + { + FolderBar.Visibility = Visibility.Collapsed; + UpdateGitBranchUi(null, "", "", "", "", Visibility.Collapsed); + RefreshContextUsageVisual(); + return; + } + + FolderBar.Visibility = Visibility.Visible; + var folder = GetCurrentWorkFolder(); + if (!string.IsNullOrEmpty(folder)) + { + FolderPathLabel.Text = folder; + FolderPathLabel.ToolTip = folder; + } + else + { + FolderPathLabel.Text = "폴더를 선택하세요"; + FolderPathLabel.ToolTip = null; + } + + LoadConversationSettings(); + LoadCompactionMetricsFromConversation(); + UpdatePermissionUI(); + UpdateDataUsageUI(); + RefreshContextUsageVisual(); + ScheduleGitBranchRefresh(); + } + + private void UpdateDataUsageUI() + { + _folderDataUsage = GetAutomaticFolderDataUsage(); + } + + private void UpdateSelectedPresetGuide(ChatConversation? conversation = null) + { + if (SelectedPresetGuide == null || SelectedPresetGuideTitle == null || SelectedPresetGuideDesc == null) + return; + + if (string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase)) + { + SelectedPresetGuide.Visibility = Visibility.Collapsed; + SelectedPresetGuideTitle.Text = ""; + SelectedPresetGuideDesc.Text = ""; + return; + } + + conversation ??= _currentConversation; + var category = conversation?.Category?.Trim(); + if (string.IsNullOrWhiteSpace(category)) + { + SelectedPresetGuide.Visibility = Visibility.Collapsed; + SelectedPresetGuideTitle.Text = ""; + SelectedPresetGuideDesc.Text = ""; + return; + } + + var preset = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets) + .FirstOrDefault(p => string.Equals(p.Category?.Trim(), category, StringComparison.OrdinalIgnoreCase)); + if (preset == null) + { + SelectedPresetGuide.Visibility = Visibility.Collapsed; + SelectedPresetGuideTitle.Text = ""; + SelectedPresetGuideDesc.Text = ""; + return; + } + + SelectedPresetGuideTitle.Text = string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase) + ? $"선택된 작업 유형 · {preset.Label}" + : $"선택된 대화 주제 · {preset.Label}"; + SelectedPresetGuideDesc.Text = string.IsNullOrWhiteSpace(preset.Description) + ? (preset.Placeholder ?? "") + : preset.Description; + SelectedPresetGuide.Visibility = Visibility.Visible; + } + + private void UpdateGitBranchUi(string? branchName, string filesText, string addedText, string deletedText, string tooltip, Visibility visibility) + { + Dispatcher.Invoke(() => + { + _currentGitBranchName = branchName; + _currentGitTooltip = tooltip; + + if (BtnGitBranch != null) + { + BtnGitBranch.Visibility = visibility; + BtnGitBranch.ToolTip = string.IsNullOrWhiteSpace(tooltip) ? "현재 Git 브랜치 상태" : tooltip; + } + + if (GitBranchLabel != null) + GitBranchLabel.Text = string.IsNullOrWhiteSpace(branchName) ? "브랜치 없음" : branchName; + if (GitBranchFilesText != null) + GitBranchFilesText.Text = filesText; + if (GitBranchAddedText != null) + GitBranchAddedText.Text = addedText; + if (GitBranchDeletedText != null) + GitBranchDeletedText.Text = deletedText; + if (GitBranchSeparator != null) + GitBranchSeparator.Visibility = visibility; + }); + } + + private void BuildGitBranchPopup() + { + if (GitBranchItems == null) + return; + + GitBranchItems.Children.Clear(); + + var gitRoot = _currentGitRoot ?? ResolveGitRoot(GetCurrentWorkFolder()); + var branchName = _currentGitBranchName ?? "detached"; + var tooltip = _currentGitTooltip ?? ""; + var fileText = GitBranchFilesText?.Text ?? ""; + var addedText = GitBranchAddedText?.Text ?? ""; + var deletedText = GitBranchDeletedText?.Text ?? ""; + var query = (_gitBranchSearchText ?? "").Trim(); + var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; + var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; + var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; + + GitBranchItems.Children.Add(CreatePopupSummaryStrip(new[] + { + ("브랜치", string.IsNullOrWhiteSpace(branchName) ? "없음" : branchName, "#F8FAFC", "#E2E8F0", "#475569"), + ("파일", string.IsNullOrWhiteSpace(fileText) ? "0" : fileText, "#EFF6FF", "#BFDBFE", "#1D4ED8"), + ("최근", _recentGitBranches.Count.ToString(), "#F5F3FF", "#DDD6FE", "#6D28D9"), + })); + GitBranchItems.Children.Add(CreatePopupSectionLabel("현재 브랜치", new Thickness(8, 6, 8, 4))); + GitBranchItems.Children.Add(CreatePopupMenuRow( + "\uE943", + branchName, + string.IsNullOrWhiteSpace(fileText) ? "현재 브랜치" : fileText, + true, + accentBrush, + secondaryText, + primaryText, + () => { })); + + if (!string.IsNullOrWhiteSpace(addedText) || !string.IsNullOrWhiteSpace(deletedText)) + { + var stats = new StackPanel + { + Orientation = Orientation.Horizontal, + Margin = new Thickness(8, 2, 8, 8), + }; + if (!string.IsNullOrWhiteSpace(addedText)) + stats.Children.Add(CreateMetricPill(addedText, "#16A34A")); + if (!string.IsNullOrWhiteSpace(deletedText)) + stats.Children.Add(CreateMetricPill(deletedText, "#DC2626")); + GitBranchItems.Children.Add(stats); + } + + if (!string.IsNullOrWhiteSpace(gitRoot)) + { + GitBranchItems.Children.Add(CreatePopupSectionLabel("저장소", new Thickness(8, 6, 8, 4))); + GitBranchItems.Children.Add(CreatePopupMenuRow( + "\uED25", + System.IO.Path.GetFileName(gitRoot.TrimEnd('\\', '/')), + gitRoot, + false, + accentBrush, + secondaryText, + primaryText, + () => { })); + } + + if (!string.IsNullOrWhiteSpace(_currentGitUpstreamStatus)) + { + GitBranchItems.Children.Add(CreatePopupMenuRow( + "\uE8AB", + "업스트림", + _currentGitUpstreamStatus!, + false, + accentBrush, + secondaryText, + primaryText, + () => { })); + } + + GitBranchItems.Children.Add(CreatePopupSectionLabel("빠른 작업", new Thickness(8, 10, 8, 4))); + GitBranchItems.Children.Add(CreatePopupMenuRow( + "\uE8C8", + "상태 요약 복사", + "브랜치, 변경 파일, 추가/삭제 라인 복사", + false, + accentBrush, + secondaryText, + primaryText, + () => + { + try { Clipboard.SetText(tooltip); } catch { } + GitBranchPopup.IsOpen = false; + })); + + GitBranchItems.Children.Add(CreatePopupMenuRow( + "\uE72B", + "새로고침", + "Git 상태를 다시 조회합니다", + false, + accentBrush, + secondaryText, + primaryText, + async () => + { + await RefreshGitBranchStatusAsync(); + BuildGitBranchPopup(); + })); + + var filteredBranches = _currentGitBranches + .Where(branch => string.IsNullOrWhiteSpace(query) + || branch.Contains(query, StringComparison.OrdinalIgnoreCase)) + .Take(20) + .ToList(); + + var recentBranches = _recentGitBranches + .Where(branch => _currentGitBranches.Any(current => string.Equals(current, branch, StringComparison.OrdinalIgnoreCase))) + .Where(branch => string.IsNullOrWhiteSpace(query) + || branch.Contains(query, StringComparison.OrdinalIgnoreCase)) + .Take(5) + .ToList(); + + if (recentBranches.Count > 0) + { + GitBranchItems.Children.Add(CreatePopupSectionLabel($"최근 전환 · {recentBranches.Count}", new Thickness(8, 10, 8, 4))); + + foreach (var branch in recentBranches) + { + var isCurrent = string.Equals(branch, branchName, StringComparison.OrdinalIgnoreCase); + GitBranchItems.Children.Add(CreatePopupMenuRow( + isCurrent ? "\uE73E" : "\uE8FD", + branch, + isCurrent ? "현재 브랜치" : "최근 사용 브랜치", + isCurrent, + accentBrush, + secondaryText, + primaryText, + isCurrent ? null : () => _ = SwitchGitBranchAsync(branch))); + } + } + + if (_currentGitBranches.Count > 0) + { + var branchSectionLabel = string.IsNullOrWhiteSpace(query) + ? $"브랜치 전환 · {_currentGitBranches.Count}" + : $"브랜치 전환 · {filteredBranches.Count}/{_currentGitBranches.Count}"; + GitBranchItems.Children.Add(CreatePopupSectionLabel(branchSectionLabel, new Thickness(8, 10, 8, 4))); + + foreach (var branch in filteredBranches) + { + if (recentBranches.Any(recent => string.Equals(recent, branch, StringComparison.OrdinalIgnoreCase))) + continue; + + var isCurrent = string.Equals(branch, branchName, StringComparison.OrdinalIgnoreCase); + GitBranchItems.Children.Add(CreatePopupMenuRow( + isCurrent ? "\uE73E" : "\uE943", + branch, + isCurrent ? "현재 브랜치" : "이 브랜치로 전환", + isCurrent, + accentBrush, + secondaryText, + primaryText, + isCurrent ? null : () => _ = SwitchGitBranchAsync(branch))); + } + + if (!string.IsNullOrWhiteSpace(query) && filteredBranches.Count == 0) + { + GitBranchItems.Children.Add(new TextBlock + { + Text = "검색 결과가 없습니다.", + FontSize = 11.5, + Foreground = secondaryText, + Margin = new Thickness(10, 6, 10, 10), + }); + } + } + + GitBranchItems.Children.Add(CreatePopupSectionLabel("브랜치 작업", new Thickness(8, 10, 8, 4))); + GitBranchItems.Children.Add(CreatePopupMenuRow( + "\uE710", + "새 브랜치 생성", + "현재 작업 기준으로 새 브랜치를 만들고 전환합니다", + false, + accentBrush, + secondaryText, + primaryText, + () => _ = CreateGitBranchAsync())); + } + + private TextBlock CreatePopupSectionLabel(string text, Thickness? margin = null) + { + return new TextBlock + { + Text = text, + FontSize = 10.5, + FontWeight = FontWeights.SemiBold, + Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, + Margin = margin ?? new Thickness(8, 8, 8, 4), + }; + } + + private UIElement CreatePopupSummaryStrip(IEnumerable<(string Label, string Value, string BgHex, string BorderHex, string FgHex)> items) + { + var wrap = new WrapPanel + { + Margin = new Thickness(8, 6, 8, 6), + }; + + foreach (var item in items) + wrap.Children.Add(CreateMetricPill($"{item.Label} {item.Value}", item.FgHex, item.BgHex, item.BorderHex)); + + return wrap; + } + + private Border CreateMetricPill(string text, string colorHex) + => CreateMetricPill(text, colorHex, $"{colorHex}18", $"{colorHex}44"); + + private Border CreateMetricPill(string text, string colorHex, string bgHex, string borderHex) + { + return new Border + { + Background = BrushFromHex(bgHex), + BorderBrush = BrushFromHex(borderHex), + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(999), + Padding = new Thickness(8, 3, 8, 3), + Margin = new Thickness(0, 0, 6, 0), + Child = new TextBlock + { + Text = text, + FontSize = 10.5, + FontWeight = FontWeights.SemiBold, + Foreground = BrushFromHex(colorHex), + } + }; + } + + private Border CreateFlatPopupRow(string icon, string title, string description, string colorHex, bool clickable, Action? onClick) + { + var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; + var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; + var hoverBrush = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.LightGray; + var borderColor = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; + + var border = new Border + { + Background = Brushes.Transparent, + BorderBrush = borderColor, + BorderThickness = new Thickness(0, 0, 0, 1), + Padding = new Thickness(8, 9, 8, 9), + Cursor = clickable ? Cursors.Hand : Cursors.Arrow, + Focusable = clickable, + }; + KeyboardNavigation.SetIsTabStop(border, clickable); + + 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 }); + + grid.Children.Add(new TextBlock + { + Text = icon, + FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontSize = 11, + Foreground = BrushFromHex(colorHex), + VerticalAlignment = VerticalAlignment.Top, + Margin = new Thickness(0, 1, 10, 0), + }); + + var textStack = new StackPanel(); + textStack.Children.Add(new TextBlock + { + Text = title, + FontSize = 12, + FontWeight = FontWeights.SemiBold, + Foreground = primaryText, + }); + if (!string.IsNullOrWhiteSpace(description)) + { + textStack.Children.Add(new TextBlock + { + Text = description, + FontSize = 10.5, + Foreground = secondaryText, + Margin = new Thickness(0, 2, 0, 0), + TextWrapping = TextWrapping.Wrap, + }); + } + + Grid.SetColumn(textStack, 1); + grid.Children.Add(textStack); + if (clickable) + { + var chevron = new TextBlock + { + Text = "\uE76C", + FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontSize = 10, + Foreground = secondaryText, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(8, 0, 0, 0), + }; + Grid.SetColumn(chevron, 2); + grid.Children.Add(chevron); + } + border.Child = grid; + + if (clickable && onClick != null) + { + border.MouseEnter += (_, _) => border.Background = hoverBrush; + border.MouseLeave += (_, _) => border.Background = Brushes.Transparent; + border.MouseLeftButtonUp += (_, _) => onClick(); + border.KeyDown += (_, ke) => + { + if (ke.Key is Key.Enter or Key.Space) + { + ke.Handled = true; + onClick(); + } + }; + } + + return border; + } +} diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 1bd4e7a..ff063d8 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -1771,37 +1771,6 @@ public partial class ChatWindow : Window } } - private void UpdateFolderBar() - { - if (FolderBar == null) return; - if (_activeTab == "Chat") - { - FolderBar.Visibility = Visibility.Collapsed; - UpdateGitBranchUi(null, "", "", "", "", Visibility.Collapsed); - RefreshContextUsageVisual(); - return; - } - FolderBar.Visibility = Visibility.Visible; - var folder = GetCurrentWorkFolder(); - if (!string.IsNullOrEmpty(folder)) - { - FolderPathLabel.Text = folder; - FolderPathLabel.ToolTip = folder; - } - else - { - FolderPathLabel.Text = "폴더를 선택하세요"; - FolderPathLabel.ToolTip = null; - } - // 대화별 설정 복원 (없으면 전역 기본값) - LoadConversationSettings(); - LoadCompactionMetricsFromConversation(); - UpdatePermissionUI(); - UpdateDataUsageUI(); - RefreshContextUsageVisual(); - ScheduleGitBranchRefresh(); - } - /// 현재 대화의 개별 설정을 로드합니다. null이면 전역 기본값 사용. private void LoadConversationSettings() { @@ -1945,11 +1914,6 @@ public partial class ChatWindow : Window _folderDataUsage = GetAutomaticFolderDataUsage(); } - private void UpdateDataUsageUI() - { - _folderDataUsage = GetAutomaticFolderDataUsage(); - } - private string GetAutomaticFolderDataUsage() => _activeTab switch { @@ -3398,48 +3362,6 @@ public partial class ChatWindow : Window UpdateSelectedPresetGuide(conversation); } - private void UpdateSelectedPresetGuide(ChatConversation? conversation = null) - { - if (SelectedPresetGuide == null || SelectedPresetGuideTitle == null || SelectedPresetGuideDesc == null) - return; - - if (string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase)) - { - SelectedPresetGuide.Visibility = Visibility.Collapsed; - SelectedPresetGuideTitle.Text = ""; - SelectedPresetGuideDesc.Text = ""; - return; - } - - conversation ??= _currentConversation; - var category = conversation?.Category?.Trim(); - if (string.IsNullOrWhiteSpace(category)) - { - SelectedPresetGuide.Visibility = Visibility.Collapsed; - SelectedPresetGuideTitle.Text = ""; - SelectedPresetGuideDesc.Text = ""; - return; - } - - var preset = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets) - .FirstOrDefault(p => string.Equals(p.Category?.Trim(), category, StringComparison.OrdinalIgnoreCase)); - if (preset == null) - { - SelectedPresetGuide.Visibility = Visibility.Collapsed; - SelectedPresetGuideTitle.Text = ""; - SelectedPresetGuideDesc.Text = ""; - return; - } - - SelectedPresetGuideTitle.Text = string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase) - ? $"선택된 작업 유형 · {preset.Label}" - : $"선택된 대화 주제 · {preset.Label}"; - SelectedPresetGuideDesc.Text = string.IsNullOrWhiteSpace(preset.Description) - ? (preset.Placeholder ?? "") - : preset.Description; - SelectedPresetGuide.Visibility = Visibility.Visible; - } - // ─── 메시지 렌더링 ─────────────────────────────────────────────────── private const int TimelineRenderPageSize = 180; @@ -17759,32 +17681,6 @@ public partial class ChatWindow : Window } } - private void UpdateGitBranchUi(string? branchName, string filesText, string addedText, string deletedText, string tooltip, Visibility visibility) - { - Dispatcher.Invoke(() => - { - _currentGitBranchName = branchName; - _currentGitTooltip = tooltip; - - if (BtnGitBranch != null) - { - BtnGitBranch.Visibility = visibility; - BtnGitBranch.ToolTip = string.IsNullOrWhiteSpace(tooltip) ? "현재 Git 브랜치 상태" : tooltip; - } - - if (GitBranchLabel != null) - GitBranchLabel.Text = string.IsNullOrWhiteSpace(branchName) ? "브랜치 없음" : branchName; - if (GitBranchFilesText != null) - GitBranchFilesText.Text = filesText; - if (GitBranchAddedText != null) - GitBranchAddedText.Text = addedText; - if (GitBranchDeletedText != null) - GitBranchDeletedText.Text = deletedText; - if (GitBranchSeparator != null) - GitBranchSeparator.Visibility = visibility; - }); - } - private static int ParseGitShortStat(string text, string unit) { if (string.IsNullOrWhiteSpace(text)) @@ -17810,332 +17706,6 @@ public partial class ChatWindow : Window return null; } - private void BuildGitBranchPopup() - { - if (GitBranchItems == null) - return; - - GitBranchItems.Children.Clear(); - - var gitRoot = _currentGitRoot ?? ResolveGitRoot(GetCurrentWorkFolder()); - var branchName = _currentGitBranchName ?? "detached"; - var tooltip = _currentGitTooltip ?? ""; - var fileText = GitBranchFilesText?.Text ?? ""; - var addedText = GitBranchAddedText?.Text ?? ""; - var deletedText = GitBranchDeletedText?.Text ?? ""; - var query = (_gitBranchSearchText ?? "").Trim(); - var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; - var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; - var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; - - GitBranchItems.Children.Add(CreatePopupSummaryStrip(new[] - { - ("브랜치", string.IsNullOrWhiteSpace(branchName) ? "없음" : branchName, "#F8FAFC", "#E2E8F0", "#475569"), - ("파일", string.IsNullOrWhiteSpace(fileText) ? "0" : fileText, "#EFF6FF", "#BFDBFE", "#1D4ED8"), - ("최근", _recentGitBranches.Count.ToString(), "#F5F3FF", "#DDD6FE", "#6D28D9"), - })); - GitBranchItems.Children.Add(CreatePopupSectionLabel("현재 브랜치", new Thickness(8, 6, 8, 4))); - GitBranchItems.Children.Add(CreatePopupMenuRow( - "\uE943", - branchName, - string.IsNullOrWhiteSpace(fileText) ? "현재 브랜치" : fileText, - true, - accentBrush, - secondaryText, - primaryText, - () => { })); - - if (!string.IsNullOrWhiteSpace(addedText) || !string.IsNullOrWhiteSpace(deletedText)) - { - var stats = new StackPanel - { - Orientation = Orientation.Horizontal, - Margin = new Thickness(8, 2, 8, 8), - }; - if (!string.IsNullOrWhiteSpace(addedText)) - stats.Children.Add(CreateMetricPill(addedText, "#16A34A")); - if (!string.IsNullOrWhiteSpace(deletedText)) - stats.Children.Add(CreateMetricPill(deletedText, "#DC2626")); - GitBranchItems.Children.Add(stats); - } - - if (!string.IsNullOrWhiteSpace(gitRoot)) - { - GitBranchItems.Children.Add(CreatePopupSectionLabel("저장소", new Thickness(8, 6, 8, 4))); - GitBranchItems.Children.Add(CreatePopupMenuRow( - "\uED25", - System.IO.Path.GetFileName(gitRoot.TrimEnd('\\', '/')), - gitRoot, - false, - accentBrush, - secondaryText, - primaryText, - () => { })); - } - - if (!string.IsNullOrWhiteSpace(_currentGitUpstreamStatus)) - { - GitBranchItems.Children.Add(CreatePopupMenuRow( - "\uE8AB", - "업스트림", - _currentGitUpstreamStatus!, - false, - accentBrush, - secondaryText, - primaryText, - () => { })); - } - - GitBranchItems.Children.Add(CreatePopupSectionLabel("빠른 작업", new Thickness(8, 10, 8, 4))); - GitBranchItems.Children.Add(CreatePopupMenuRow( - "\uE8C8", - "상태 요약 복사", - "브랜치, 변경 파일, 추가/삭제 라인 복사", - false, - accentBrush, - secondaryText, - primaryText, - () => - { - try { Clipboard.SetText(tooltip); } catch { } - GitBranchPopup.IsOpen = false; - })); - - GitBranchItems.Children.Add(CreatePopupMenuRow( - "\uE72B", - "새로고침", - "Git 상태를 다시 조회합니다", - false, - accentBrush, - secondaryText, - primaryText, - async () => - { - await RefreshGitBranchStatusAsync(); - BuildGitBranchPopup(); - })); - - var filteredBranches = _currentGitBranches - .Where(branch => string.IsNullOrWhiteSpace(query) - || branch.Contains(query, StringComparison.OrdinalIgnoreCase)) - .Take(20) - .ToList(); - - var recentBranches = _recentGitBranches - .Where(branch => _currentGitBranches.Any(current => string.Equals(current, branch, StringComparison.OrdinalIgnoreCase))) - .Where(branch => string.IsNullOrWhiteSpace(query) - || branch.Contains(query, StringComparison.OrdinalIgnoreCase)) - .Take(5) - .ToList(); - - if (recentBranches.Count > 0) - { - var recentSectionLabel = string.IsNullOrWhiteSpace(query) - ? $"최근 전환 · {recentBranches.Count}" - : $"최근 전환 · {recentBranches.Count}"; - GitBranchItems.Children.Add(CreatePopupSectionLabel(recentSectionLabel, new Thickness(8, 10, 8, 4))); - - foreach (var branch in recentBranches) - { - var isCurrent = string.Equals(branch, branchName, StringComparison.OrdinalIgnoreCase); - GitBranchItems.Children.Add(CreatePopupMenuRow( - isCurrent ? "\uE73E" : "\uE8FD", - branch, - isCurrent ? "현재 브랜치" : "최근 사용 브랜치", - isCurrent, - accentBrush, - secondaryText, - primaryText, - isCurrent ? null : () => _ = SwitchGitBranchAsync(branch))); - } - } - - if (_currentGitBranches.Count > 0) - { - var branchSectionLabel = string.IsNullOrWhiteSpace(query) - ? $"브랜치 전환 · {_currentGitBranches.Count}" - : $"브랜치 전환 · {filteredBranches.Count}/{_currentGitBranches.Count}"; - GitBranchItems.Children.Add(CreatePopupSectionLabel(branchSectionLabel, new Thickness(8, 10, 8, 4))); - - foreach (var branch in filteredBranches) - { - if (recentBranches.Any(recent => string.Equals(recent, branch, StringComparison.OrdinalIgnoreCase))) - continue; - - var isCurrent = string.Equals(branch, branchName, StringComparison.OrdinalIgnoreCase); - GitBranchItems.Children.Add(CreatePopupMenuRow( - isCurrent ? "\uE73E" : "\uE943", - branch, - isCurrent ? "현재 브랜치" : "이 브랜치로 전환", - isCurrent, - accentBrush, - secondaryText, - primaryText, - isCurrent ? null : () => _ = SwitchGitBranchAsync(branch))); - } - - if (!string.IsNullOrWhiteSpace(query) && filteredBranches.Count == 0) - { - GitBranchItems.Children.Add(new TextBlock - { - Text = "검색 결과가 없습니다.", - FontSize = 11.5, - Foreground = secondaryText, - Margin = new Thickness(10, 6, 10, 10), - }); - } - } - - GitBranchItems.Children.Add(CreatePopupSectionLabel("브랜치 작업", new Thickness(8, 10, 8, 4))); - GitBranchItems.Children.Add(CreatePopupMenuRow( - "\uE710", - "새 브랜치 생성", - "현재 작업 기준으로 새 브랜치를 만들고 전환합니다", - false, - accentBrush, - secondaryText, - primaryText, - () => _ = CreateGitBranchAsync())); - } - - private TextBlock CreatePopupSectionLabel(string text, Thickness? margin = null) - { - return new TextBlock - { - Text = text, - FontSize = 10.5, - FontWeight = FontWeights.SemiBold, - Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, - Margin = margin ?? new Thickness(8, 8, 8, 4), - }; - } - - private UIElement CreatePopupSummaryStrip(IEnumerable<(string Label, string Value, string BgHex, string BorderHex, string FgHex)> items) - { - var wrap = new WrapPanel - { - Margin = new Thickness(8, 6, 8, 6), - }; - - foreach (var item in items) - { - wrap.Children.Add(CreateMetricPill($"{item.Label} {item.Value}", item.FgHex, item.BgHex, item.BorderHex)); - } - - return wrap; - } - - private Border CreateMetricPill(string text, string colorHex) - => CreateMetricPill(text, colorHex, $"{colorHex}18", $"{colorHex}44"); - - private Border CreateMetricPill(string text, string colorHex, string bgHex, string borderHex) - { - return new Border - { - Background = BrushFromHex(bgHex), - BorderBrush = BrushFromHex(borderHex), - BorderThickness = new Thickness(1), - CornerRadius = new CornerRadius(999), - Padding = new Thickness(8, 3, 8, 3), - Margin = new Thickness(0, 0, 6, 0), - Child = new TextBlock - { - Text = text, - FontSize = 10.5, - FontWeight = FontWeights.SemiBold, - Foreground = BrushFromHex(colorHex), - } - }; - } - - private Border CreateFlatPopupRow(string icon, string title, string description, string colorHex, bool clickable, Action? onClick) - { - var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; - var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; - var hoverBrush = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.LightGray; - var borderColor = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; - - var border = new Border - { - Background = Brushes.Transparent, - BorderBrush = borderColor, - BorderThickness = new Thickness(0, 0, 0, 1), - Padding = new Thickness(8, 9, 8, 9), - Cursor = clickable ? Cursors.Hand : Cursors.Arrow, - Focusable = clickable, - }; - KeyboardNavigation.SetIsTabStop(border, clickable); - - 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 }); - - grid.Children.Add(new TextBlock - { - Text = icon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), - FontSize = 11, - Foreground = BrushFromHex(colorHex), - VerticalAlignment = VerticalAlignment.Top, - Margin = new Thickness(0, 1, 10, 0), - }); - - var textStack = new StackPanel(); - textStack.Children.Add(new TextBlock - { - Text = title, - FontSize = 12, - FontWeight = FontWeights.SemiBold, - Foreground = primaryText, - }); - if (!string.IsNullOrWhiteSpace(description)) - { - textStack.Children.Add(new TextBlock - { - Text = description, - FontSize = 10.5, - Foreground = secondaryText, - Margin = new Thickness(0, 2, 0, 0), - TextWrapping = TextWrapping.Wrap, - }); - } - Grid.SetColumn(textStack, 1); - grid.Children.Add(textStack); - if (clickable) - { - var chevron = new TextBlock - { - Text = "\uE76C", - FontFamily = new FontFamily("Segoe MDL2 Assets"), - FontSize = 10, - Foreground = secondaryText, - VerticalAlignment = VerticalAlignment.Center, - Margin = new Thickness(8, 0, 0, 0), - }; - Grid.SetColumn(chevron, 2); - grid.Children.Add(chevron); - } - border.Child = grid; - - if (clickable && onClick != null) - { - border.MouseEnter += (_, _) => border.Background = hoverBrush; - border.MouseLeave += (_, _) => border.Background = Brushes.Transparent; - border.MouseLeftButtonUp += (_, _) => onClick(); - border.KeyDown += (_, ke) => - { - if (ke.Key is Key.Enter or Key.Space) - { - ke.Handled = true; - onClick(); - } - }; - } - - return border; - } - private async Task SwitchGitBranchAsync(string branchName) { if (string.IsNullOrWhiteSpace(branchName) || string.IsNullOrWhiteSpace(_currentGitRoot))