AX Agent 대화 목록 렌더를 별도 프레젠테이션 파일로 분리하고 문서 이력을 갱신한다
Some checks failed
Release Gate / gate (push) Has been cancelled
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:
@@ -1178,3 +1178,7 @@ MIT License
|
||||
- 업데이트: 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 변경 뒤 어떤 묶음을 확인해야 하는지 바로 쓸 수 있게 정리했다.
|
||||
- 업데이트: 2026-04-06 10:07 (KST)
|
||||
- 프리셋 카드와 주제 선택 흐름을 [ChatWindow.TopicPresetPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TopicPresetPresentation.cs) 로 분리했다. `BuildTopicButtons`, `ShowCustomPresetDialog`, `ShowCustomPresetContextMenu`, `SelectTopic`이 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 밖으로 이동해, 프리셋 UI와 대화 orchestration의 책임 경계가 더 분명해졌다.
|
||||
- 업데이트: 2026-04-06 10:18 (KST)
|
||||
- 좌측 대화 목록 렌더를 [ChatWindow.ConversationListPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs) 로 분리했다. `RefreshConversationList`, `RenderConversationList`, `AddLoadMoreButton`, `BuildConversationSpotlightItems`, `AddGroupHeader`, `AddConversationItem`이 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 밖으로 이동해, 메인 창은 transcript/runtime orchestration에 더 집중하고 목록 UI는 별도 presentation surface에서 관리되게 정리했다.
|
||||
|
||||
@@ -4921,3 +4921,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
|
||||
- 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.
|
||||
- Document update: 2026-04-06 10:07 (KST) - Split topic preset rendering and selection flow out of `ChatWindow.xaml.cs` into `ChatWindow.TopicPresetPresentation.cs`. Preset card creation, custom preset dialogs/context menus, and `SelectTopic(...)` metadata application now live in a dedicated partial.
|
||||
- Document update: 2026-04-06 10:07 (KST) - This keeps the main chat window more orchestration-focused and narrows the remaining maintainability work to small follow-up polish rather than any large structural split.
|
||||
- Document update: 2026-04-06 10:18 (KST) - Split conversation list rendering out of `ChatWindow.xaml.cs` into `ChatWindow.ConversationListPresentation.cs`. Conversation meta filtering, spotlight calculation, grouped list rendering, load-more pagination, and sidebar conversation-row assembly now live in a dedicated partial.
|
||||
- Document update: 2026-04-06 10:18 (KST) - This keeps the main chat window more focused on transcript/runtime orchestration while isolating sidebar list presentation for future UX tuning and parity work.
|
||||
|
||||
525
src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs
Normal file
525
src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs
Normal file
@@ -0,0 +1,525 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private const int ConversationPageSize = 50;
|
||||
private List<ConversationMeta>? _pendingConversations;
|
||||
|
||||
public void RefreshConversationList()
|
||||
{
|
||||
var metas = _storage.LoadAllMeta();
|
||||
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);
|
||||
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 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 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 stack = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center };
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"더 보기 ({remaining}개 남음)",
|
||||
FontSize = 12,
|
||||
Foreground = accentBrush,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
btn.Child = stack;
|
||||
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)
|
||||
return;
|
||||
|
||||
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",
|
||||
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;
|
||||
|
||||
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)
|
||||
return;
|
||||
|
||||
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.BeginInvoke(new Action(() => ShowConversationMenu(item.Id)), DispatcherPriority.Input);
|
||||
};
|
||||
|
||||
ConversationPanel.Children.Add(border);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user