AX Agent 대화 목록 렌더를 별도 프레젠테이션 파일로 분리하고 문서 이력을 갱신한다
Some checks failed
Release Gate / gate (push) Has been cancelled

좌측 대화 목록의 메타 필터링, spotlight 계산, 그룹 렌더, 더 보기 페이지네이션, 목록 카드 조립을 ChatWindow.ConversationListPresentation.cs로 이동해 ChatWindow.xaml.cs가 transcript/runtime orchestration에 더 집중하도록 정리했다.

README와 DEVELOPMENT 문서에 2026-04-06 10:18 (KST) 기준 변경 이력을 반영했고, dotnet build 검증 결과 경고 0 / 오류 0을 확인했다.
This commit is contained in:
2026-04-06 10:23:25 +09:00
parent 1b4566d192
commit 8faa26b134
4 changed files with 531 additions and 511 deletions

View File

@@ -2084,517 +2084,6 @@ public partial class ChatWindow : Window
// ─── 대화 목록 ────────────────────────────────────────────────────────
public void RefreshConversationList()
{
var metas = _storage.LoadAllMeta();
// 프리셋 카테고리 → 아이콘/색상 매핑 (ChatCategory에 없는 코워크/코드 카테고리 지원)
var allPresets = Services.PresetService.GetByTabWithCustom("Cowork", _settings.Settings.Llm.CustomPresets)
.Concat(Services.PresetService.GetByTabWithCustom("Code", _settings.Settings.Llm.CustomPresets))
.Concat(Services.PresetService.GetByTabWithCustom("Chat", _settings.Settings.Llm.CustomPresets));
var presetMap = new Dictionary<string, (string Symbol, string Color)>(StringComparer.OrdinalIgnoreCase);
foreach (var p in allPresets)
presetMap.TryAdd(p.Category, (p.Symbol, p.Color));
var items = metas.Select(c =>
{
var symbol = ChatCategory.GetSymbol(c.Category);
var color = ChatCategory.GetColor(c.Category);
// ChatCategory 기본값이면 프리셋에서 검색
if (symbol == "\uE8BD" && color == "#6B7280" && c.Category != ChatCategory.General)
{
if (presetMap.TryGetValue(c.Category, out var pm))
{
symbol = pm.Symbol;
color = pm.Color;
}
}
var runSummary = _appState.GetConversationRunSummary(c.AgentRunHistory);
return new ConversationMeta
{
Id = c.Id,
Title = c.Title,
Pinned = c.Pinned,
Category = c.Category,
Symbol = symbol,
ColorHex = color,
Tab = NormalizeTabName(c.Tab),
UpdatedAtText = FormatDate(c.UpdatedAt),
UpdatedAt = c.UpdatedAt,
Preview = c.Preview ?? "",
ParentId = c.ParentId,
AgentRunCount = runSummary.AgentRunCount,
FailedAgentRunCount = runSummary.FailedAgentRunCount,
LastAgentRunSummary = runSummary.LastAgentRunSummary,
LastFailedAt = runSummary.LastFailedAt,
LastCompletedAt = runSummary.LastCompletedAt,
WorkFolder = c.WorkFolder ?? "",
IsRunning = _currentConversation?.Id == c.Id
&& !string.IsNullOrWhiteSpace(_appState.AgentRun.RunId)
&& !string.Equals(_appState.AgentRun.Status, "completed", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(_appState.AgentRun.Status, "failed", StringComparison.OrdinalIgnoreCase),
};
}).ToList();
// 탭 필터 — 현재 활성 탭의 대화만 표시
items = items.Where(i => string.Equals(i.Tab, _activeTab, StringComparison.OrdinalIgnoreCase)).ToList();
// 탭 전환 과정에서 저장된 "빈 새 대화" 노이즈 항목은 목록에서 숨김
items = items.Where(i =>
i.Pinned
|| !string.IsNullOrWhiteSpace(i.ParentId)
|| !string.Equals((i.Title ?? "").Trim(), "새 대화", StringComparison.OrdinalIgnoreCase)
|| !string.IsNullOrWhiteSpace(i.Preview)
|| i.AgentRunCount > 0
|| i.FailedAgentRunCount > 0
|| !string.Equals(i.Category, ChatCategory.General, StringComparison.OrdinalIgnoreCase)
).ToList();
_failedConversationCount = items.Count(i => i.FailedAgentRunCount > 0);
_runningConversationCount = items.Count(i => i.IsRunning);
_spotlightConversationCount = items.Count(i => i.FailedAgentRunCount > 0 || i.AgentRunCount >= 3);
UpdateConversationFailureFilterUi();
UpdateConversationRunningFilterUi();
UpdateConversationQuickStripUi();
// 상단 필터 적용
if (_activeTab == "Cowork")
{
if (!string.IsNullOrEmpty(_selectedCategory))
{
items = items.Where(i => string.Equals(i.Category, _selectedCategory, StringComparison.OrdinalIgnoreCase)).ToList();
}
}
else if (_activeTab == "Code")
{
if (!string.IsNullOrEmpty(_selectedCategory))
{
items = items.Where(i => string.Equals(i.WorkFolder, _selectedCategory, StringComparison.OrdinalIgnoreCase)).ToList();
}
}
else
{
if (_selectedCategory == "__custom__")
{
// 커스텀 프리셋으로 만든 대화만 표시
var customCats = _settings.Settings.Llm.CustomPresets
.Select(c => $"custom_{c.Id}").ToHashSet();
items = items.Where(i => customCats.Contains(i.Category)).ToList();
}
else if (!string.IsNullOrEmpty(_selectedCategory))
{
items = items.Where(i => i.Category == _selectedCategory).ToList();
}
}
// 검색 필터 (제목 + 내용 미리보기)
var search = SearchBox?.Text?.Trim() ?? "";
if (!string.IsNullOrEmpty(search))
items = items.Where(i =>
i.Title.Contains(search, StringComparison.OrdinalIgnoreCase) ||
i.Preview.Contains(search, StringComparison.OrdinalIgnoreCase) ||
i.LastAgentRunSummary.Contains(search, StringComparison.OrdinalIgnoreCase)
).ToList();
if (_runningOnlyFilter)
items = items.Where(i => i.IsRunning).ToList();
items = (_sortConversationsByRecent
? items.OrderByDescending(i => i.Pinned)
.ThenByDescending(i => i.UpdatedAt)
.ThenByDescending(i => i.FailedAgentRunCount > 0)
.ThenByDescending(i => i.AgentRunCount)
: items.OrderByDescending(i => i.Pinned)
.ThenByDescending(i => i.FailedAgentRunCount > 0)
.ThenByDescending(i => i.AgentRunCount)
.ThenByDescending(i => i.UpdatedAt))
.ToList();
RenderConversationList(items);
}
private const int ConversationPageSize = 50;
private List<ConversationMeta>? _pendingConversations;
private void RenderConversationList(List<ConversationMeta> items)
{
ConversationPanel.Children.Clear();
_pendingConversations = null;
if (items.Count == 0)
{
var emptyText = _activeTab switch
{
"Cowork" => "Cowork 탭 대화가 없습니다",
"Code" => "Code 탭 대화가 없습니다",
_ => "Chat 탭 대화가 없습니다",
};
var empty = new TextBlock
{
Text = emptyText,
FontSize = 12,
Foreground = (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray),
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 20, 0, 0)
};
ConversationPanel.Children.Add(empty);
return;
}
var spotlightItems = BuildConversationSpotlightItems(items);
if (spotlightItems.Count > 0)
{
AddGroupHeader("집중 필요");
foreach (var item in spotlightItems)
AddConversationItem(item);
ConversationPanel.Children.Add(new Border
{
Height = 1,
Margin = new Thickness(10, 8, 10, 4),
Background = BrushFromHex("#E5E7EB"),
Opacity = 0.7,
});
}
var allOrdered = new List<(string Group, ConversationMeta Item)>();
foreach (var item in items)
allOrdered.Add((GetConversationDateGroup(item.UpdatedAt), item));
// 첫 페이지만 렌더링
var firstPage = allOrdered.Take(ConversationPageSize).ToList();
string? lastGroup = null;
foreach (var (group, item) in firstPage)
{
if (group != lastGroup) { AddGroupHeader(group); lastGroup = group; }
AddConversationItem(item);
}
// 나머지가 있으면 "더 보기" 버튼
if (allOrdered.Count > ConversationPageSize)
{
_pendingConversations = items;
AddLoadMoreButton(allOrdered.Count - ConversationPageSize);
}
}
private void AddLoadMoreButton(int remaining)
{
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
var btn = new Border
{
Background = Brushes.Transparent,
CornerRadius = new CornerRadius(8),
Cursor = Cursors.Hand,
Padding = new Thickness(8, 10, 8, 10),
Margin = new Thickness(6, 4, 6, 4),
HorizontalAlignment = HorizontalAlignment.Stretch,
};
var sp = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center };
sp.Children.Add(new TextBlock
{
Text = $"더 보기 ({remaining}개 남음)",
FontSize = 12,
Foreground = accentBrush,
HorizontalAlignment = HorizontalAlignment.Center,
});
btn.Child = sp;
btn.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x12, 0xFF, 0xFF, 0xFF)); };
btn.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
btn.MouseLeftButtonUp += (_, _) =>
{
// 전체 목록 렌더링
if (_pendingConversations != null)
{
var all = _pendingConversations;
_pendingConversations = null;
ConversationPanel.Children.Clear();
string? lastGroup = null;
foreach (var item in all)
{
var group = GetConversationDateGroup(item.UpdatedAt);
if (!string.Equals(lastGroup, group, StringComparison.Ordinal))
{
AddGroupHeader(group);
lastGroup = group;
}
AddConversationItem(item);
}
}
};
ConversationPanel.Children.Add(btn);
}
private static string GetConversationDateGroup(DateTime updatedAt)
{
var today = DateTime.Today;
var date = updatedAt.Date;
if (date == today)
return "오늘";
if (date == today.AddDays(-1))
return "어제";
return "이전";
}
private List<ConversationMeta> BuildConversationSpotlightItems(List<ConversationMeta> items)
{
if (_failedOnlyFilter || _runningOnlyFilter)
return new List<ConversationMeta>();
var search = SearchBox?.Text?.Trim() ?? "";
if (!string.IsNullOrEmpty(search))
return new List<ConversationMeta>();
return items
.Where(i => i.FailedAgentRunCount > 0 || i.AgentRunCount >= 3)
.OrderByDescending(i => i.FailedAgentRunCount)
.ThenByDescending(i => i.AgentRunCount)
.ThenByDescending(i => i.UpdatedAt)
.Take(3)
.ToList();
}
private void AddGroupHeader(string text)
{
var header = new TextBlock
{
Text = text,
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray),
Margin = new Thickness(8, 10, 0, 4)
};
ConversationPanel.Children.Add(header);
}
private void AddConversationItem(ConversationMeta item)
{
var isSelected = false;
lock (_convLock)
isSelected = _currentConversation?.Id == item.Id;
var isBranch = !string.IsNullOrEmpty(item.ParentId);
var border = new Border
{
Background = isSelected
? new SolidColorBrush(Color.FromArgb(0x10, 0x4B, 0x5E, 0xFC))
: Brushes.Transparent,
CornerRadius = new CornerRadius(5),
Padding = new Thickness(7, 4.5, 7, 4.5),
Margin = isBranch ? new Thickness(10, 1, 0, 1) : new Thickness(0, 1, 0, 1),
Cursor = Cursors.Hand
};
var grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(16) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
// 카테고리 아이콘 (고정 시 핀 아이콘, 그 외 카테고리 색상)
Brush iconBrush;
if (item.Pinned)
iconBrush = Brushes.Orange;
else
{
try { iconBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(item.ColorHex)); }
catch { iconBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue; }
}
var iconText = item.Pinned ? "\uE718" : !string.IsNullOrEmpty(item.ParentId) ? "\uE8A5" : item.Symbol;
if (!string.IsNullOrEmpty(item.ParentId)) iconBrush = new SolidColorBrush(Color.FromRgb(0x8B, 0x5C, 0xF6)); // 분기: 보라색
var icon = new TextBlock
{
Text = iconText,
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 10.5,
Foreground = iconBrush,
VerticalAlignment = VerticalAlignment.Center
};
Grid.SetColumn(icon, 0);
grid.Children.Add(icon);
// 제목 + 날짜 (선택 시 약간 밝게)
var titleColor = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var dateColor = TryFindResource("HintText") as Brush ?? Brushes.DarkGray;
var stack = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
var title = new TextBlock
{
Text = item.Title,
FontSize = 11.75,
FontWeight = isSelected ? FontWeights.SemiBold : FontWeights.Normal,
Foreground = titleColor,
TextTrimming = TextTrimming.CharacterEllipsis
};
var date = new TextBlock
{
Text = item.UpdatedAtText,
FontSize = 9,
Foreground = dateColor,
Margin = new Thickness(0, 1.5, 0, 0)
};
stack.Children.Add(title);
stack.Children.Add(date);
if (item.IsRunning)
{
stack.Children.Add(new TextBlock
{
Text = _appState.ActiveTasks.Count > 0
? $"진행 중 {_appState.ActiveTasks.Count}"
: "진행 중",
FontSize = 8.8,
FontWeight = FontWeights.Medium,
Foreground = BrushFromHex("#4F46E5"),
Margin = new Thickness(0, 1.5, 0, 0),
});
}
if (item.AgentRunCount > 0)
{
var runSummaryText = new TextBlock
{
Text = item.FailedAgentRunCount > 0
? $"실패 {item.FailedAgentRunCount} · {TruncateForStatus(item.LastAgentRunSummary, 26)}"
: $"실행 {item.AgentRunCount} · {TruncateForStatus(item.LastAgentRunSummary, 28)}",
FontSize = 8.9,
Foreground = item.FailedAgentRunCount > 0
? BrushFromHex("#B91C1C")
: (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray),
Margin = new Thickness(0, 1.5, 0, 0),
TextTrimming = TextTrimming.CharacterEllipsis
};
if (!string.IsNullOrWhiteSpace(item.LastAgentRunSummary))
{
runSummaryText.ToolTip = item.FailedAgentRunCount > 0
? $"최근 실패 포함\n{item.LastAgentRunSummary}"
: item.LastAgentRunSummary;
}
stack.Children.Add(runSummaryText);
}
Grid.SetColumn(stack, 1);
grid.Children.Add(stack);
// 카테고리 변경 버튼 (호버 시 표시)
var catBtn = new Button
{
Content = new TextBlock
{
Text = "\uE70F", // Edit
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 9,
Foreground = (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray)
},
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Cursor = Cursors.Hand,
VerticalAlignment = VerticalAlignment.Center,
Visibility = Visibility.Collapsed,
Width = 20,
Height = 20,
Padding = new Thickness(0),
Opacity = 0.72,
ToolTip = _activeTab == "Cowork" ? "작업 유형" : "대화 주제 변경"
};
var capturedId = item.Id;
catBtn.Click += (_, _) => ShowConversationMenu(capturedId);
Grid.SetColumn(catBtn, 2);
grid.Children.Add(catBtn);
// 선택 시 좌측 액센트 바
if (isSelected)
{
border.BorderBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
border.BorderThickness = new Thickness(1.25, 0, 0, 0);
}
border.Child = grid;
// 호버 이벤트 — 배경만 얇게 강조
border.RenderTransformOrigin = new Point(0.5, 0.5);
border.RenderTransform = new ScaleTransform(1, 1);
var hoverBg = new SolidColorBrush(Color.FromArgb(0x08, 0xFF, 0xFF, 0xFF));
border.MouseEnter += (_, _) =>
{
if (!isSelected)
border.Background = hoverBg;
catBtn.Visibility = Visibility.Visible;
};
border.MouseLeave += (_, _) =>
{
if (!isSelected)
border.Background = Brushes.Transparent;
catBtn.Visibility = Visibility.Collapsed;
};
// 클릭 — 이미 선택된 대화면 제목 편집, 아니면 대화 전환
border.MouseLeftButtonDown += (_, _) =>
{
try
{
if (isSelected)
{
// 이미 선택된 대화 → 제목 편집 모드
EnterTitleEditMode(title, item.Id, titleColor);
return;
}
// 스트리밍 중이면 취소
if (_isStreaming)
{
_streamCts?.Cancel();
_cursorTimer.Stop();
_typingTimer.Stop();
_elapsedTimer.Stop();
_activeStreamText = null;
_elapsedLabel = null;
_isStreaming = false;
}
var conv = _storage.Load(item.Id);
if (conv != null)
{
lock (_convLock)
{
_currentConversation = ChatSession?.SetCurrentConversation(_activeTab, conv, _storage) ?? conv;
SyncTabConversationIdsFromSession();
}
SaveLastConversations();
UpdateChatTitle();
RenderMessages();
RefreshConversationList();
RefreshDraftQueueUi();
}
}
catch (Exception ex)
{
LogService.Error($"대화 전환 오류: {ex.Message}");
}
};
// 우클릭 → 대화 관리 메뉴 바로 표시
border.MouseRightButtonUp += (_, me) =>
{
me.Handled = true;
// 선택되지 않은 대화를 우클릭하면 먼저 선택
if (!isSelected)
{
var conv = _storage.Load(item.Id);
if (conv != null)
{
lock (_convLock)
{
_currentConversation = ChatSession?.SetCurrentConversation(_activeTab, conv, _storage) ?? conv;
SyncTabConversationIdsFromSession();
}
SaveLastConversations();
UpdateChatTitle();
RenderMessages();
RefreshDraftQueueUi();
}
}
// Dispatcher로 지연 호출 — 마우스 이벤트 완료 후 Popup 열기
Dispatcher.BeginInvoke(new Action(() => ShowConversationMenu(item.Id)), DispatcherPriority.Input);
};
ConversationPanel.Children.Add(border);
}
// ─── 대화 제목 인라인 편집 ────────────────────────────────────────────
private void EnterTitleEditMode(TextBlock titleTb, string conversationId, Brush titleColor)