AX Agent footer와 Git 브랜치 프레젠테이션 구조를 분리하고 회귀 점검 루틴을 고정한다
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:
2026-04-06 10:06:25 +09:00
parent 9464dd0234
commit d0d66c1d52
6 changed files with 420 additions and 369 deletions

View File

@@ -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 변경 뒤 어떤 묶음을 확인해야 하는지 바로 쓸 수 있게 정리했다.

View File

@@ -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
- 체크 후 문서 이력에는 “어떤 묶음을 확인했는지”를 간단히 남깁니다.

View File

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

View File

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

View File

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

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