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:
2026-04-06 08:14:01 +09:00
parent 82b42b3ba3
commit b4a506de96
4 changed files with 447 additions and 430 deletions

View File

@@ -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>
private void LoadConversationSettings()
{
@@ -1945,11 +1914,6 @@ public partial class ChatWindow : Window
_folderDataUsage = GetAutomaticFolderDataUsage();
}
private void UpdateDataUsageUI()
{
_folderDataUsage = GetAutomaticFolderDataUsage();
}
private string GetAutomaticFolderDataUsage()
=> _activeTab switch
{
@@ -3398,48 +3362,6 @@ public partial class ChatWindow : Window
UpdateSelectedPresetGuide(conversation);
}
private void UpdateSelectedPresetGuide(ChatConversation? conversation = null)
{
if (SelectedPresetGuide == null || SelectedPresetGuideTitle == null || SelectedPresetGuideDesc == null)
return;
if (string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase))
{
SelectedPresetGuide.Visibility = Visibility.Collapsed;
SelectedPresetGuideTitle.Text = "";
SelectedPresetGuideDesc.Text = "";
return;
}
conversation ??= _currentConversation;
var category = conversation?.Category?.Trim();
if (string.IsNullOrWhiteSpace(category))
{
SelectedPresetGuide.Visibility = Visibility.Collapsed;
SelectedPresetGuideTitle.Text = "";
SelectedPresetGuideDesc.Text = "";
return;
}
var preset = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets)
.FirstOrDefault(p => string.Equals(p.Category?.Trim(), category, StringComparison.OrdinalIgnoreCase));
if (preset == null)
{
SelectedPresetGuide.Visibility = Visibility.Collapsed;
SelectedPresetGuideTitle.Text = "";
SelectedPresetGuideDesc.Text = "";
return;
}
SelectedPresetGuideTitle.Text = string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase)
? $"선택된 작업 유형 · {preset.Label}"
: $"선택된 대화 주제 · {preset.Label}";
SelectedPresetGuideDesc.Text = string.IsNullOrWhiteSpace(preset.Description)
? (preset.Placeholder ?? "")
: preset.Description;
SelectedPresetGuide.Visibility = Visibility.Visible;
}
// ─── 메시지 렌더링 ───────────────────────────────────────────────────
private const int TimelineRenderPageSize = 180;
@@ -17759,32 +17681,6 @@ public partial class ChatWindow : Window
}
}
private void UpdateGitBranchUi(string? branchName, string filesText, string addedText, string deletedText, string tooltip, Visibility visibility)
{
Dispatcher.Invoke(() =>
{
_currentGitBranchName = branchName;
_currentGitTooltip = tooltip;
if (BtnGitBranch != null)
{
BtnGitBranch.Visibility = visibility;
BtnGitBranch.ToolTip = string.IsNullOrWhiteSpace(tooltip) ? "현재 Git 브랜치 상태" : tooltip;
}
if (GitBranchLabel != null)
GitBranchLabel.Text = string.IsNullOrWhiteSpace(branchName) ? "브랜치 없음" : branchName;
if (GitBranchFilesText != null)
GitBranchFilesText.Text = filesText;
if (GitBranchAddedText != null)
GitBranchAddedText.Text = addedText;
if (GitBranchDeletedText != null)
GitBranchDeletedText.Text = deletedText;
if (GitBranchSeparator != null)
GitBranchSeparator.Visibility = visibility;
});
}
private static int ParseGitShortStat(string text, string unit)
{
if (string.IsNullOrWhiteSpace(text))
@@ -17810,332 +17706,6 @@ public partial class ChatWindow : Window
return null;
}
private void BuildGitBranchPopup()
{
if (GitBranchItems == null)
return;
GitBranchItems.Children.Clear();
var gitRoot = _currentGitRoot ?? ResolveGitRoot(GetCurrentWorkFolder());
var branchName = _currentGitBranchName ?? "detached";
var tooltip = _currentGitTooltip ?? "";
var fileText = GitBranchFilesText?.Text ?? "";
var addedText = GitBranchAddedText?.Text ?? "";
var deletedText = GitBranchDeletedText?.Text ?? "";
var query = (_gitBranchSearchText ?? "").Trim();
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
GitBranchItems.Children.Add(CreatePopupSummaryStrip(new[]
{
("브랜치", string.IsNullOrWhiteSpace(branchName) ? "없음" : branchName, "#F8FAFC", "#E2E8F0", "#475569"),
("파일", string.IsNullOrWhiteSpace(fileText) ? "0" : fileText, "#EFF6FF", "#BFDBFE", "#1D4ED8"),
("최근", _recentGitBranches.Count.ToString(), "#F5F3FF", "#DDD6FE", "#6D28D9"),
}));
GitBranchItems.Children.Add(CreatePopupSectionLabel("현재 브랜치", new Thickness(8, 6, 8, 4)));
GitBranchItems.Children.Add(CreatePopupMenuRow(
"\uE943",
branchName,
string.IsNullOrWhiteSpace(fileText) ? "현재 브랜치" : fileText,
true,
accentBrush,
secondaryText,
primaryText,
() => { }));
if (!string.IsNullOrWhiteSpace(addedText) || !string.IsNullOrWhiteSpace(deletedText))
{
var stats = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(8, 2, 8, 8),
};
if (!string.IsNullOrWhiteSpace(addedText))
stats.Children.Add(CreateMetricPill(addedText, "#16A34A"));
if (!string.IsNullOrWhiteSpace(deletedText))
stats.Children.Add(CreateMetricPill(deletedText, "#DC2626"));
GitBranchItems.Children.Add(stats);
}
if (!string.IsNullOrWhiteSpace(gitRoot))
{
GitBranchItems.Children.Add(CreatePopupSectionLabel("저장소", new Thickness(8, 6, 8, 4)));
GitBranchItems.Children.Add(CreatePopupMenuRow(
"\uED25",
System.IO.Path.GetFileName(gitRoot.TrimEnd('\\', '/')),
gitRoot,
false,
accentBrush,
secondaryText,
primaryText,
() => { }));
}
if (!string.IsNullOrWhiteSpace(_currentGitUpstreamStatus))
{
GitBranchItems.Children.Add(CreatePopupMenuRow(
"\uE8AB",
"업스트림",
_currentGitUpstreamStatus!,
false,
accentBrush,
secondaryText,
primaryText,
() => { }));
}
GitBranchItems.Children.Add(CreatePopupSectionLabel("빠른 작업", new Thickness(8, 10, 8, 4)));
GitBranchItems.Children.Add(CreatePopupMenuRow(
"\uE8C8",
"상태 요약 복사",
"브랜치, 변경 파일, 추가/삭제 라인 복사",
false,
accentBrush,
secondaryText,
primaryText,
() =>
{
try { Clipboard.SetText(tooltip); } catch { }
GitBranchPopup.IsOpen = false;
}));
GitBranchItems.Children.Add(CreatePopupMenuRow(
"\uE72B",
"새로고침",
"Git 상태를 다시 조회합니다",
false,
accentBrush,
secondaryText,
primaryText,
async () =>
{
await RefreshGitBranchStatusAsync();
BuildGitBranchPopup();
}));
var filteredBranches = _currentGitBranches
.Where(branch => string.IsNullOrWhiteSpace(query)
|| branch.Contains(query, StringComparison.OrdinalIgnoreCase))
.Take(20)
.ToList();
var recentBranches = _recentGitBranches
.Where(branch => _currentGitBranches.Any(current => string.Equals(current, branch, StringComparison.OrdinalIgnoreCase)))
.Where(branch => string.IsNullOrWhiteSpace(query)
|| branch.Contains(query, StringComparison.OrdinalIgnoreCase))
.Take(5)
.ToList();
if (recentBranches.Count > 0)
{
var recentSectionLabel = string.IsNullOrWhiteSpace(query)
? $"최근 전환 · {recentBranches.Count}"
: $"최근 전환 · {recentBranches.Count}";
GitBranchItems.Children.Add(CreatePopupSectionLabel(recentSectionLabel, new Thickness(8, 10, 8, 4)));
foreach (var branch in recentBranches)
{
var isCurrent = string.Equals(branch, branchName, StringComparison.OrdinalIgnoreCase);
GitBranchItems.Children.Add(CreatePopupMenuRow(
isCurrent ? "\uE73E" : "\uE8FD",
branch,
isCurrent ? "현재 브랜치" : "최근 사용 브랜치",
isCurrent,
accentBrush,
secondaryText,
primaryText,
isCurrent ? null : () => _ = SwitchGitBranchAsync(branch)));
}
}
if (_currentGitBranches.Count > 0)
{
var branchSectionLabel = string.IsNullOrWhiteSpace(query)
? $"브랜치 전환 · {_currentGitBranches.Count}"
: $"브랜치 전환 · {filteredBranches.Count}/{_currentGitBranches.Count}";
GitBranchItems.Children.Add(CreatePopupSectionLabel(branchSectionLabel, new Thickness(8, 10, 8, 4)));
foreach (var branch in filteredBranches)
{
if (recentBranches.Any(recent => string.Equals(recent, branch, StringComparison.OrdinalIgnoreCase)))
continue;
var isCurrent = string.Equals(branch, branchName, StringComparison.OrdinalIgnoreCase);
GitBranchItems.Children.Add(CreatePopupMenuRow(
isCurrent ? "\uE73E" : "\uE943",
branch,
isCurrent ? "현재 브랜치" : "이 브랜치로 전환",
isCurrent,
accentBrush,
secondaryText,
primaryText,
isCurrent ? null : () => _ = SwitchGitBranchAsync(branch)));
}
if (!string.IsNullOrWhiteSpace(query) && filteredBranches.Count == 0)
{
GitBranchItems.Children.Add(new TextBlock
{
Text = "검색 결과가 없습니다.",
FontSize = 11.5,
Foreground = secondaryText,
Margin = new Thickness(10, 6, 10, 10),
});
}
}
GitBranchItems.Children.Add(CreatePopupSectionLabel("브랜치 작업", new Thickness(8, 10, 8, 4)));
GitBranchItems.Children.Add(CreatePopupMenuRow(
"\uE710",
"새 브랜치 생성",
"현재 작업 기준으로 새 브랜치를 만들고 전환합니다",
false,
accentBrush,
secondaryText,
primaryText,
() => _ = CreateGitBranchAsync()));
}
private TextBlock CreatePopupSectionLabel(string text, Thickness? margin = null)
{
return new TextBlock
{
Text = text,
FontSize = 10.5,
FontWeight = FontWeights.SemiBold,
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
Margin = margin ?? new Thickness(8, 8, 8, 4),
};
}
private UIElement CreatePopupSummaryStrip(IEnumerable<(string Label, string Value, string BgHex, string BorderHex, string FgHex)> items)
{
var wrap = new WrapPanel
{
Margin = new Thickness(8, 6, 8, 6),
};
foreach (var item in items)
{
wrap.Children.Add(CreateMetricPill($"{item.Label} {item.Value}", item.FgHex, item.BgHex, item.BorderHex));
}
return wrap;
}
private Border CreateMetricPill(string text, string colorHex)
=> CreateMetricPill(text, colorHex, $"{colorHex}18", $"{colorHex}44");
private Border CreateMetricPill(string text, string colorHex, string bgHex, string borderHex)
{
return new Border
{
Background = BrushFromHex(bgHex),
BorderBrush = BrushFromHex(borderHex),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(999),
Padding = new Thickness(8, 3, 8, 3),
Margin = new Thickness(0, 0, 6, 0),
Child = new TextBlock
{
Text = text,
FontSize = 10.5,
FontWeight = FontWeights.SemiBold,
Foreground = BrushFromHex(colorHex),
}
};
}
private Border CreateFlatPopupRow(string icon, string title, string description, string colorHex, bool clickable, Action? onClick)
{
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var hoverBrush = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.LightGray;
var borderColor = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var border = new Border
{
Background = Brushes.Transparent,
BorderBrush = borderColor,
BorderThickness = new Thickness(0, 0, 0, 1),
Padding = new Thickness(8, 9, 8, 9),
Cursor = clickable ? Cursors.Hand : Cursors.Arrow,
Focusable = clickable,
};
KeyboardNavigation.SetIsTabStop(border, clickable);
var grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
grid.Children.Add(new TextBlock
{
Text = icon,
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 11,
Foreground = BrushFromHex(colorHex),
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 1, 10, 0),
});
var textStack = new StackPanel();
textStack.Children.Add(new TextBlock
{
Text = title,
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
});
if (!string.IsNullOrWhiteSpace(description))
{
textStack.Children.Add(new TextBlock
{
Text = description,
FontSize = 10.5,
Foreground = secondaryText,
Margin = new Thickness(0, 2, 0, 0),
TextWrapping = TextWrapping.Wrap,
});
}
Grid.SetColumn(textStack, 1);
grid.Children.Add(textStack);
if (clickable)
{
var chevron = new TextBlock
{
Text = "\uE76C",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 10,
Foreground = secondaryText,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(8, 0, 0, 0),
};
Grid.SetColumn(chevron, 2);
grid.Children.Add(chevron);
}
border.Child = grid;
if (clickable && onClick != null)
{
border.MouseEnter += (_, _) => border.Background = hoverBrush;
border.MouseLeave += (_, _) => border.Background = Brushes.Transparent;
border.MouseLeftButtonUp += (_, _) => onClick();
border.KeyDown += (_, ke) =>
{
if (ke.Key is Key.Enter or Key.Space)
{
ke.Handled = true;
onClick();
}
};
}
return border;
}
private async Task SwitchGitBranchAsync(string branchName)
{
if (string.IsNullOrWhiteSpace(branchName) || string.IsNullOrWhiteSpace(_currentGitRoot))