From d0d66c1d52512dec3abd75c9fa3071a34cac4b4a Mon Sep 17 00:00:00 2001 From: lacvet Date: Mon, 6 Apr 2026 10:06:25 +0900 Subject: [PATCH] =?UTF-8?q?AX=20Agent=20footer=EC=99=80=20Git=20=EB=B8=8C?= =?UTF-8?q?=EB=9E=9C=EC=B9=98=20=ED=94=84=EB=A0=88=EC=A0=A0=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EA=B5=AC=EC=A1=B0=EB=A5=BC=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=ED=95=98=EA=B3=A0=20=ED=9A=8C=EA=B7=80=20=EC=A0=90?= =?UTF-8?q?=EA=B2=80=20=EB=A3=A8=ED=8B=B4=EC=9D=84=20=EA=B3=A0=EC=A0=95?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChatWindow.FooterPresentation에서 Git 브랜치 팝업 렌더와 요약 helper를 분리해 폴더 바 상태/프리셋 안내 동기화 책임만 남긴다 - ChatWindow.GitBranchPresentation을 추가해 브랜치 팝업 조립, 요약 pill, 최근 브랜치/전환 액션 렌더를 별도 프레젠테이션 계층으로 옮긴다 - AX_AGENT_REGRESSION_PROMPTS 문서를 재작성해 실패 분류와 Chat/Cowork/Code별 필수 회귀 묶음을 개발 루틴으로 고정한다 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0) --- README.md | 3 + docs/AX_AGENT_REGRESSION_PROMPTS.md | 65 +++- docs/DEVELOPMENT.md | 2 + docs/claw-code-parity-plan.md | 3 + .../Views/ChatWindow.FooterPresentation.cs | 355 +---------------- .../Views/ChatWindow.GitBranchPresentation.cs | 361 ++++++++++++++++++ 6 files changed, 420 insertions(+), 369 deletions(-) create mode 100644 src/AxCopilot/Views/ChatWindow.GitBranchPresentation.cs diff --git a/README.md b/README.md index 1cc681d..1bde8fa 100644 --- a/README.md +++ b/README.md @@ -1175,3 +1175,6 @@ MIT License - 업데이트: 2026-04-06 09:44 (KST) - inline interaction renderer를 `의견 요청`과 `계획 승인`으로 다시 분리했다. [ChatWindow.UserAskPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.UserAskPresentation.cs)에 사용자 질문 카드 렌더를, [ChatWindow.PlanApprovalPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.PlanApprovalPresentation.cs)에 계획 승인/상세창 연동 흐름을 옮겨 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 메시지 타입 책임을 더 줄였다. - 이번 단계까지 완료된 계획 항목은 `상태선 카탈로그화`, `권한/도구 결과 카탈로그 정교화`, `권한 UI 정리`, `의견 요청/계획 승인 renderer 분리`다. 남은 큰 축은 `footer/composer를 더 작업 바 중심으로 정리`와 `회귀 프롬프트 세트의 개발 루틴 고정`이다. +- 업데이트: 2026-04-06 09:58 (KST) + - footer/composer 구조 개선의 다음 단계로 Git 브랜치 팝업과 footer 요약 helper를 [ChatWindow.GitBranchPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.GitBranchPresentation.cs) 로 분리했다. [ChatWindow.FooterPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs)는 이제 폴더 바 상태와 선택된 프리셋 안내처럼 footer의 현재 상태 동기화 책임만 남긴다. + - [AX_AGENT_REGRESSION_PROMPTS.md](/E:/AX%20Copilot%20-%20Codex/docs/AX_AGENT_REGRESSION_PROMPTS.md)를 개발 루틴 문서로 강화했다. Chat/Cowork/Code 공통 프롬프트 세트에 `blank-reply`, `duplicate-banner`, `bad-approval-flow`, `queue-drift`, `restore-drift`, `status-noise` 실패 분류를 붙여, runtime/transcript 변경 뒤 어떤 묶음을 확인해야 하는지 바로 쓸 수 있게 정리했다. diff --git a/docs/AX_AGENT_REGRESSION_PROMPTS.md b/docs/AX_AGENT_REGRESSION_PROMPTS.md index 7d6453e..458fe75 100644 --- a/docs/AX_AGENT_REGRESSION_PROMPTS.md +++ b/docs/AX_AGENT_REGRESSION_PROMPTS.md @@ -1,22 +1,38 @@ # AX Agent Regression Prompts -업데이트: 2026-04-05 22:18 (KST) +업데이트: 2026-04-06 09:58 (KST) -`claw-code`와 AX Agent를 같은 기준으로 비교하기 위한 회귀 프롬프트 세트입니다. +`claw-code`와 AX Agent를 같은 기준으로 비교하기 위한 공통 회귀 프롬프트 세트입니다. + +## 사용 규칙 + +- 런타임 동작, transcript 렌더, 권한/계획/질문 UX, queue/compact/reopen 흐름에 영향을 주는 변경 뒤에는 이 문서를 기준으로 최소 1회 점검합니다. +- 모든 항목을 매번 수동 실행할 필요는 없지만, 관련 축이 바뀌었으면 해당 묶음은 반드시 확인합니다. +- 결과는 “문장이 똑같은가”가 아니라 “실행 경로와 사용자 체감 결과가 같은가”를 봅니다. + +## 실패 분류 + +- `blank-reply`: 토큰은 소비됐는데 본문이 비어 있거나 assistant 카드가 비어 있음 +- `duplicate-banner`: 같은 실행 이벤트가 transcript에 중복 표시됨 +- `bad-approval-flow`: 권한/계획/질문 요청이 inline으로 안 닫히고 popup 의존이 커짐 +- `queue-drift`: 후속 요청, retry, regenerate가 다른 실행 경로를 타거나 순서가 어긋남 +- `restore-drift`: reopen 후 상태선, queue, 최신 메시지 상태가 달라짐 +- `status-noise`: Cowork/Code 기본 상태선이 과하게 흔들리거나 debug 정보가 과노출됨 ## Chat -1. 기본 답변 +1. 기본 응답 - 프롬프트: `회의 일정 조정 메일을 정중한 한국어로 써줘` - 확인: - - 빈 assistant 카드 없음 - - 재생성/재시도 후 transcript 중복 없음 + - `blank-reply` + - `restore-drift` 2. 장문 설명 - 프롬프트: `RAG와 fine-tuning 차이를 실무 관점으로 7가지로 설명해줘` - 확인: - - 장문 렌더 유지 + - 장문 렌더 안정성 - compact 이후 다음 턴 문맥 유지 + - `blank-reply` ## Cowork @@ -25,14 +41,16 @@ - 확인: - 작업 유형 반영 - 계획 이후 실제 문서형 결과 흐름 - - 기본 로그 과다 노출 없음 + - 기본 로그 과노출 없음 + - `bad-approval-flow` 4. 데이터형 작업 - 프롬프트: `매출 CSV를 분석해서 월별 추세와 이상치를 요약해줘` - 확인: - - 데이터 분석형 도구 선택 - - 결과 요약 품질 + - 데이터 분석 도구 선택 + - 결과 요약 일관성 - runtime 노이즈 최소화 + - `status-noise` ## Code @@ -40,40 +58,53 @@ - 프롬프트: `현재 프로젝트에서 설정 저장 버그 원인 찾고 수정해줘` - 확인: - 읽기/검색/수정 흐름 일관성 - - diff 저장 - - reopen 시 transcript 보존 + - diff/저장/재오픈 시 transcript 보존 + - `restore-drift` 6. 빌드/테스트 - 프롬프트: `빌드 오류를 재현하고 수정한 뒤 다시 빌드해줘` - 확인: - build/test 루프 - 실패 후 재시도 - - 완료 메시지 정합성 + - 완료 메시지 일관성 + - `queue-drift` ## Cross-tab -7. 후속 큐 -- 순차 프롬프트: +7. 후속 요청 +- 프롬프트 순서: - `이 창 레이아웃 문제 원인 찾아줘` - `끝나면 README도 같이 갱신해줘` - 확인: - queue chaining - - 입력창 직접 변형 없이 다음 턴 수행 + - 입력창 직접 변경 없이 다음 턴 실행 + - `queue-drift` 8. compact 이후 연속성 - 프롬프트: `지금까지 논의한 내용을 5줄로 이어서 정리하고 다음 작업 제안해줘` - 확인: - token-only completion 없음 - compact 후 문맥 유지 + - `queue-drift` 9. 권한 승인 - 프롬프트: `이 파일을 수정해서 저장해줘` - 확인: - - 승인 요청 transcript 표시 - - 승인/거부 후 결과 정합성 + - 권한 요청 transcript 표시 + - 승인/거부 결과 일관성 + - `bad-approval-flow` 10. slash / skill - 프롬프트: `/bug-hunt src 폴더 잠재 버그 찾아줘` - 확인: - slash 진입과 일반 send 경로 동일성 - skill 실행 이유/결과 표기 + - `queue-drift` + +## 개발 루틴 고정 + +- transcript, permission, tool-result, queue, compact, reopen에 영향을 주는 변경은 커밋 전 아래를 기준으로 셀프 체크합니다. + - Chat 변경: 1, 2, 8 + - Cowork 변경: 3, 4, 7, 8 + - Code 변경: 5, 6, 7, 9, 10 +- 체크 후 문서 이력에는 “어떤 묶음을 확인했는지”를 간단히 남깁니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index faf0927..ca01b2c 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4917,3 +4917,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - Document update: 2026-04-06 09:36 (KST) - Removed the stale `Plan` option from `PermissionModePresentationCatalog.cs` and simplified `ChatWindow.PermissionPresentation.cs` so the permission popup and top-banner presentation only expose the live modes (`권한 요청`, `편집 자동 승인`, `권한 건너뛰기`, `읽기 전용`). - Document update: 2026-04-06 09:44 (KST) - Split inline interaction rendering further by replacing `ChatWindow.InlineInteractions.cs` with `ChatWindow.UserAskPresentation.cs` and `ChatWindow.PlanApprovalPresentation.cs`. User-question cards and plan approval/detail flows now live in dedicated partials instead of sharing one mixed interaction file. - Document update: 2026-04-06 09:44 (KST) - At this point the completed structure-improvement items are: status presentation cataloging, permission/tool-result catalog enrichment, permission UI cleanup, and ask/plan renderer separation. The remaining larger tracks are footer/composer work-bar refinement and enforcing the regression prompt ritual in day-to-day development. +- Document update: 2026-04-06 09:58 (KST) - Split Git branch popup assembly and footer-adjacent summary helpers out of `ChatWindow.FooterPresentation.cs` into `ChatWindow.GitBranchPresentation.cs`. The footer presentation partial now focuses on folder-bar state and selected-preset guide synchronization instead of mixed popup rendering. +- Document update: 2026-04-06 09:58 (KST) - Rewrote `docs/AX_AGENT_REGRESSION_PROMPTS.md` into a repeatable regression ritual with explicit failure classes (`blank-reply`, `duplicate-banner`, `bad-approval-flow`, `queue-drift`, `restore-drift`, `status-noise`) and required prompt bundles per change area so runtime/transcript work can be validated consistently. diff --git a/docs/claw-code-parity-plan.md b/docs/claw-code-parity-plan.md index dc40047..33d178f 100644 --- a/docs/claw-code-parity-plan.md +++ b/docs/claw-code-parity-plan.md @@ -14,6 +14,9 @@ - Progressed the maintainability track by moving runtime strip styling into `OperationalStatusPresentationCatalog.cs`, expanding permission/tool-result transcript catalogs with typed descriptions, and removing the stale plan-mode presentation branch from permission UI surfaces. The next structural focus remains footer/status/composer presentation slimming and regression ritual enforcement. - Updated: 2026-04-06 09:44 (KST) - Continued the maintainability track by splitting mixed inline interaction rendering into `ChatWindow.UserAskPresentation.cs` and `ChatWindow.PlanApprovalPresentation.cs`. This reduces message-type coupling inside the main window and keeps the next focus on footer/composer presentation and regression-routine formalization. +- Updated: 2026-04-06 09:58 (KST) +- Continued the maintainability track by splitting Git branch popup and footer-adjacent summary helpers into `ChatWindow.GitBranchPresentation.cs`, leaving `ChatWindow.FooterPresentation.cs` focused on folder bar state and preset-guide sync only. +- Formalized the regression ritual in `docs/AX_AGENT_REGRESSION_PROMPTS.md` by adding failure classes (`blank-reply`, `duplicate-banner`, `bad-approval-flow`, `queue-drift`, `restore-drift`, `status-noise`) and required prompt bundles per change area. ## Preserved History (Summary) - Core loop guards and post-tool verification gates are already partially implemented. diff --git a/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs b/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs index e9c54ee..c6bfc05 100644 --- a/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs +++ b/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs @@ -1,11 +1,8 @@ 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; @@ -14,7 +11,9 @@ public partial class ChatWindow { private void UpdateFolderBar() { - if (FolderBar == null) return; + if (FolderBar == null) + return; + if (_activeTab == "Chat") { FolderBar.Visibility = Visibility.Collapsed; @@ -90,352 +89,4 @@ public partial class ChatWindow : 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.GitBranchPresentation.cs b/src/AxCopilot/Views/ChatWindow.GitBranchPresentation.cs new file mode 100644 index 0000000..139d5df --- /dev/null +++ b/src/AxCopilot/Views/ChatWindow.GitBranchPresentation.cs @@ -0,0 +1,361 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace AxCopilot.Views; + +public partial class ChatWindow +{ + 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 += (_, keyEvent) => + { + if (keyEvent.Key is Key.Enter or Key.Space) + { + keyEvent.Handled = true; + onClick(); + } + }; + } + + return border; + } +}