AX Agent footer와 Git 브랜치 프레젠테이션 구조를 분리하고 회귀 점검 루틴을 고정한다
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
- 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)
This commit is contained in:
@@ -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 변경 뒤 어떤 묶음을 확인해야 하는지 바로 쓸 수 있게 정리했다.
|
||||
|
||||
@@ -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
|
||||
- 체크 후 문서 이력에는 “어떤 묶음을 확인했는지”를 간단히 남깁니다.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
361
src/AxCopilot/Views/ChatWindow.GitBranchPresentation.cs
Normal file
361
src/AxCopilot/Views/ChatWindow.GitBranchPresentation.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user