AX Agent 대화 목록 관리 프레젠테이션 분리 및 구조 개선 마감\n\n- ChatWindow에서 제목 인라인 편집과 대화 관리 팝업 렌더를 ConversationManagementPresentation partial로 분리\n- 고정, 이름 변경, 카테고리 변경, 삭제 등 대화 목록 관리 상호작용을 메인 창 orchestration 코드 밖으로 이동\n- README와 DEVELOPMENT 문서에 구조 개선 완료 범위와 남은 작업 수준을 2026-04-06 10:56 (KST) 기준으로 반영\n- dotnet build 검증 경고 0, 오류 0 확인
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
This commit is contained in:
@@ -2084,416 +2084,6 @@ public partial class ChatWindow : Window
|
||||
|
||||
// ─── 대화 목록 ────────────────────────────────────────────────────────
|
||||
|
||||
// ─── 대화 제목 인라인 편집 ────────────────────────────────────────────
|
||||
|
||||
private void EnterTitleEditMode(TextBlock titleTb, string conversationId, Brush titleColor)
|
||||
{
|
||||
try
|
||||
{
|
||||
// titleTb가 이미 부모에서 분리된 경우(편집 중) 무시
|
||||
var parent = titleTb.Parent as StackPanel;
|
||||
if (parent == null) return;
|
||||
|
||||
var idx = parent.Children.IndexOf(titleTb);
|
||||
if (idx < 0) return;
|
||||
|
||||
var editBox = new TextBox
|
||||
{
|
||||
Text = titleTb.Text,
|
||||
FontSize = 12.5,
|
||||
Foreground = titleColor,
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue,
|
||||
BorderThickness = new Thickness(0, 0, 0, 1),
|
||||
CaretBrush = titleColor,
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
};
|
||||
|
||||
// 안전하게 자식 교체: 먼저 제거 후 삽입
|
||||
parent.Children.RemoveAt(idx);
|
||||
parent.Children.Insert(idx, editBox);
|
||||
|
||||
var committed = false;
|
||||
void CommitEdit()
|
||||
{
|
||||
if (committed) return;
|
||||
committed = true;
|
||||
|
||||
var newTitle = editBox.Text.Trim();
|
||||
if (string.IsNullOrEmpty(newTitle)) newTitle = titleTb.Text;
|
||||
|
||||
titleTb.Text = newTitle;
|
||||
// editBox가 아직 parent에 있는지 확인 후 교체
|
||||
try
|
||||
{
|
||||
var currentIdx = parent.Children.IndexOf(editBox);
|
||||
if (currentIdx >= 0)
|
||||
{
|
||||
parent.Children.RemoveAt(currentIdx);
|
||||
parent.Children.Insert(currentIdx, titleTb);
|
||||
}
|
||||
}
|
||||
catch { /* 부모가 이미 해제된 경우 무시 */ }
|
||||
|
||||
var conv = _storage.Load(conversationId);
|
||||
if (conv != null)
|
||||
{
|
||||
conv.Title = newTitle;
|
||||
_storage.Save(conv);
|
||||
lock (_convLock)
|
||||
{
|
||||
if (_currentConversation?.Id == conversationId)
|
||||
_currentConversation = ChatSession?.UpdateConversationMetadata(_activeTab, c => c.Title = newTitle, _storage) ?? _currentConversation;
|
||||
}
|
||||
UpdateChatTitle();
|
||||
}
|
||||
}
|
||||
|
||||
void CancelEdit()
|
||||
{
|
||||
if (committed) return;
|
||||
committed = true;
|
||||
try
|
||||
{
|
||||
var currentIdx = parent.Children.IndexOf(editBox);
|
||||
if (currentIdx >= 0)
|
||||
{
|
||||
parent.Children.RemoveAt(currentIdx);
|
||||
parent.Children.Insert(currentIdx, titleTb);
|
||||
}
|
||||
}
|
||||
catch { /* 부모가 이미 해제된 경우 무시 */ }
|
||||
}
|
||||
|
||||
editBox.KeyDown += (_, ke) =>
|
||||
{
|
||||
if (ke.Key == Key.Enter) { ke.Handled = true; CommitEdit(); }
|
||||
if (ke.Key == Key.Escape) { ke.Handled = true; CancelEdit(); }
|
||||
};
|
||||
editBox.LostFocus += (_, _) => CommitEdit();
|
||||
|
||||
editBox.Focus();
|
||||
editBox.SelectAll();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Error($"제목 편집 오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 카테고리 변경 팝업 ──────────────────────────────────────────────
|
||||
|
||||
private void ShowConversationMenu(string conversationId)
|
||||
{
|
||||
var conv = _storage.Load(conversationId);
|
||||
var isPinned = conv?.Pinned ?? false;
|
||||
|
||||
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(30, 255, 255, 255));
|
||||
|
||||
var popup = new Popup
|
||||
{
|
||||
StaysOpen = false,
|
||||
AllowsTransparency = true,
|
||||
PopupAnimation = PopupAnimation.Fade,
|
||||
Placement = PlacementMode.MousePoint,
|
||||
};
|
||||
|
||||
var container = new Border
|
||||
{
|
||||
Background = bgBrush,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(6),
|
||||
MinWidth = 200,
|
||||
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||||
{
|
||||
BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black
|
||||
},
|
||||
};
|
||||
|
||||
var stack = new StackPanel();
|
||||
|
||||
// 메뉴 항목 헬퍼
|
||||
Border CreateMenuItem(string icon, string text, Brush iconColor, Action onClick)
|
||||
{
|
||||
var item = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(10, 7, 10, 7),
|
||||
Margin = new Thickness(0, 1, 0, 1),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var g = new Grid();
|
||||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(24) });
|
||||
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
|
||||
var iconTb = new TextBlock
|
||||
{
|
||||
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12, Foreground = iconColor, VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(iconTb, 0);
|
||||
g.Children.Add(iconTb);
|
||||
|
||||
var textTb = new TextBlock
|
||||
{
|
||||
Text = text, FontSize = 12.5, Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(textTb, 1);
|
||||
g.Children.Add(textTb);
|
||||
|
||||
item.Child = g;
|
||||
item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
|
||||
item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||||
item.MouseLeftButtonUp += (_, _) => { popup.IsOpen = false; onClick(); };
|
||||
return item;
|
||||
}
|
||||
|
||||
Border CreateSeparator() => new()
|
||||
{
|
||||
Height = 1, Background = borderBrush, Opacity = 0.3, Margin = new Thickness(8, 4, 8, 4),
|
||||
};
|
||||
|
||||
// 고정/해제
|
||||
stack.Children.Add(CreateMenuItem(
|
||||
isPinned ? "\uE77A" : "\uE718",
|
||||
isPinned ? "고정 해제" : "상단 고정",
|
||||
TryFindResource("AccentColor") as Brush ?? Brushes.Blue,
|
||||
() =>
|
||||
{
|
||||
var c = _storage.Load(conversationId);
|
||||
if (c != null)
|
||||
{
|
||||
c.Pinned = !c.Pinned;
|
||||
_storage.Save(c);
|
||||
lock (_convLock)
|
||||
{
|
||||
if (_currentConversation?.Id == conversationId)
|
||||
{
|
||||
_currentConversation = ChatSession?.UpdateConversationMetadata(_activeTab, current => current.Pinned = c.Pinned, _storage) ?? _currentConversation;
|
||||
}
|
||||
}
|
||||
RefreshConversationList();
|
||||
}
|
||||
}));
|
||||
|
||||
// 이름 변경
|
||||
stack.Children.Add(CreateMenuItem("\uE8AC", "이름 변경", secondaryText, () =>
|
||||
{
|
||||
// 대화 목록에서 해당 항목 찾아서 편집 모드 진입
|
||||
foreach (UIElement child in ConversationPanel.Children)
|
||||
{
|
||||
if (child is Border b && b.Child is Grid g)
|
||||
{
|
||||
foreach (UIElement gc in g.Children)
|
||||
{
|
||||
if (gc is StackPanel sp && sp.Children.Count > 0 && sp.Children[0] is TextBlock tb)
|
||||
{
|
||||
// title과 매칭
|
||||
if (conv != null && tb.Text == conv.Title)
|
||||
{
|
||||
var titleColor = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
EnterTitleEditMode(tb, conversationId, titleColor);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Cowork/Code 탭: 작업 유형 읽기 전용 표시
|
||||
if ((_activeTab == "Cowork" || _activeTab == "Code") && conv != null)
|
||||
{
|
||||
var catKey = conv.Category ?? ChatCategory.General;
|
||||
// ChatCategory 또는 프리셋에서 아이콘/라벨 검색
|
||||
string catSymbol = "\uE8BD", catLabel = catKey, catColor = "#6B7280";
|
||||
var chatCat = ChatCategory.All.FirstOrDefault(c => c.Key == catKey);
|
||||
if (chatCat != default && chatCat.Key != ChatCategory.General)
|
||||
{
|
||||
catSymbol = chatCat.Symbol; catLabel = chatCat.Label; catColor = chatCat.Color;
|
||||
}
|
||||
else
|
||||
{
|
||||
var preset = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets)
|
||||
.FirstOrDefault(p => p.Category == catKey);
|
||||
if (preset != null)
|
||||
{
|
||||
catSymbol = preset.Symbol; catLabel = preset.Label; catColor = preset.Color;
|
||||
}
|
||||
}
|
||||
|
||||
stack.Children.Add(CreateSeparator());
|
||||
var infoSp = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(10, 4, 10, 4) };
|
||||
try
|
||||
{
|
||||
var catBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(catColor));
|
||||
infoSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = catSymbol, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12, Foreground = catBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0),
|
||||
});
|
||||
infoSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = catLabel, FontSize = 12, Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
infoSp.Children.Add(new TextBlock { Text = catLabel, FontSize = 12, Foreground = primaryText });
|
||||
}
|
||||
stack.Children.Add(infoSp);
|
||||
}
|
||||
|
||||
// Chat 탭만 분류 변경 표시 (Cowork/Code 탭은 분류 불필요)
|
||||
var showCategorySection = _activeTab == "Chat";
|
||||
|
||||
if (showCategorySection)
|
||||
{
|
||||
stack.Children.Add(CreateSeparator());
|
||||
|
||||
// 분류 헤더
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "분류 변경",
|
||||
FontSize = 10.5,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(10, 4, 0, 4),
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
});
|
||||
|
||||
var currentCategory = conv?.Category ?? ChatCategory.General;
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
|
||||
foreach (var (key, label, symbol, color) in ChatCategory.All)
|
||||
{
|
||||
var capturedKey = key;
|
||||
var isCurrentCat = capturedKey == currentCategory;
|
||||
|
||||
// 카테고리 항목 (체크 표시 포함)
|
||||
var catItem = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(10, 7, 10, 7),
|
||||
Margin = new Thickness(0, 1, 0, 1),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var catGrid = new Grid();
|
||||
catGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(24) });
|
||||
catGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
catGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(20) });
|
||||
|
||||
var catIcon = new TextBlock
|
||||
{
|
||||
Text = symbol, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12, Foreground = BrushFromHex(color), VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(catIcon, 0);
|
||||
catGrid.Children.Add(catIcon);
|
||||
|
||||
var catText = new TextBlock
|
||||
{
|
||||
Text = label, FontSize = 12.5, Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
FontWeight = isCurrentCat ? FontWeights.Bold : FontWeights.Normal,
|
||||
};
|
||||
Grid.SetColumn(catText, 1);
|
||||
catGrid.Children.Add(catText);
|
||||
|
||||
if (isCurrentCat)
|
||||
{
|
||||
var check = CreateSimpleCheck(accentBrush, 14);
|
||||
Grid.SetColumn(check, 2);
|
||||
catGrid.Children.Add(check);
|
||||
}
|
||||
|
||||
catItem.Child = catGrid;
|
||||
catItem.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
|
||||
catItem.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||||
catItem.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
popup.IsOpen = false;
|
||||
var c = _storage.Load(conversationId);
|
||||
if (c != null)
|
||||
{
|
||||
c.Category = capturedKey;
|
||||
var preset = Services.PresetService.GetByCategory(capturedKey);
|
||||
if (preset != null)
|
||||
c.SystemCommand = preset.SystemPrompt;
|
||||
_storage.Save(c);
|
||||
lock (_convLock)
|
||||
{
|
||||
if (_currentConversation?.Id == conversationId)
|
||||
{
|
||||
_currentConversation = ChatSession?.UpdateConversationMetadata(_activeTab, current =>
|
||||
{
|
||||
current.Category = capturedKey;
|
||||
if (preset != null)
|
||||
current.SystemCommand = preset.SystemPrompt;
|
||||
}, _storage) ?? _currentConversation;
|
||||
}
|
||||
}
|
||||
// 현재 대화의 카테고리가 변경되면 입력 안내 문구도 갱신
|
||||
bool isCurrent;
|
||||
lock (_convLock) { isCurrent = _currentConversation?.Id == conversationId; }
|
||||
if (isCurrent && preset != null && !string.IsNullOrEmpty(preset.Placeholder))
|
||||
{
|
||||
_promptCardPlaceholder = preset.Placeholder;
|
||||
UpdateWatermarkVisibility();
|
||||
if (string.IsNullOrEmpty(InputBox.Text))
|
||||
{
|
||||
InputWatermark.Text = preset.Placeholder;
|
||||
InputWatermark.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
else if (isCurrent)
|
||||
{
|
||||
ClearPromptCardPlaceholder();
|
||||
}
|
||||
RefreshConversationList();
|
||||
}
|
||||
};
|
||||
stack.Children.Add(catItem);
|
||||
}
|
||||
} // end showCategorySection
|
||||
|
||||
stack.Children.Add(CreateSeparator());
|
||||
|
||||
// 삭제
|
||||
stack.Children.Add(CreateMenuItem("\uE74D", "이 대화 삭제", Brushes.IndianRed, () =>
|
||||
{
|
||||
var result = CustomMessageBox.Show("이 대화를 삭제하시겠습니까?", "대화 삭제",
|
||||
MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
if (result != MessageBoxResult.Yes) return;
|
||||
_storage.Delete(conversationId);
|
||||
lock (_convLock)
|
||||
{
|
||||
if (_currentConversation?.Id == conversationId)
|
||||
{
|
||||
_currentConversation = null;
|
||||
MessagePanel.Children.Clear();
|
||||
EmptyState.Visibility = Visibility.Visible;
|
||||
UpdateChatTitle();
|
||||
}
|
||||
}
|
||||
RefreshConversationList();
|
||||
}));
|
||||
|
||||
container.Child = stack;
|
||||
popup.Child = container;
|
||||
popup.IsOpen = true;
|
||||
}
|
||||
|
||||
// ─── 검색 ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user