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