diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index ca01b2c..dbd5232 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -4919,3 +4919,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
- Document update: 2026-04-06 09:44 (KST) - At this point the completed structure-improvement items are: status presentation cataloging, permission/tool-result catalog enrichment, permission UI cleanup, and ask/plan renderer separation. The remaining larger tracks are footer/composer work-bar refinement and enforcing the regression prompt ritual in day-to-day development.
- Document update: 2026-04-06 09:58 (KST) - Split Git branch popup assembly and footer-adjacent summary helpers out of `ChatWindow.FooterPresentation.cs` into `ChatWindow.GitBranchPresentation.cs`. The footer presentation partial now focuses on folder-bar state and selected-preset guide synchronization instead of mixed popup rendering.
- 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.
diff --git a/docs/claw-code-parity-plan.md b/docs/claw-code-parity-plan.md
index 33d178f..5dfd472 100644
--- a/docs/claw-code-parity-plan.md
+++ b/docs/claw-code-parity-plan.md
@@ -17,6 +17,8 @@
- Updated: 2026-04-06 09:58 (KST)
- Continued the maintainability track by splitting Git branch popup and footer-adjacent summary helpers into `ChatWindow.GitBranchPresentation.cs`, leaving `ChatWindow.FooterPresentation.cs` focused on folder bar state and preset-guide sync only.
- Formalized the regression ritual in `docs/AX_AGENT_REGRESSION_PROMPTS.md` by adding failure classes (`blank-reply`, `duplicate-banner`, `bad-approval-flow`, `queue-drift`, `restore-drift`, `status-noise`) and required prompt bundles per change area.
+- Updated: 2026-04-06 10:07 (KST)
+- Continued the maintainability track by moving topic preset rendering, custom preset context menus, and topic-selection application flow into `ChatWindow.TopicPresetPresentation.cs`. This reduces mixed preset UI logic inside `ChatWindow.xaml.cs` and keeps the main window closer to orchestration-only responsibility.
## Preserved History (Summary)
- Core loop guards and post-tool verification gates are already partially implemented.
diff --git a/src/AxCopilot/Views/ChatWindow.TopicPresetPresentation.cs b/src/AxCopilot/Views/ChatWindow.TopicPresetPresentation.cs
new file mode 100644
index 0000000..d8abbf9
--- /dev/null
+++ b/src/AxCopilot/Views/ChatWindow.TopicPresetPresentation.cs
@@ -0,0 +1,523 @@
+using System;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Input;
+using System.Windows.Media;
+using AxCopilot.Models;
+using AxCopilot.Services;
+
+namespace AxCopilot.Views;
+
+public partial class ChatWindow
+{
+ /// 프리셋에서 대화 주제 버튼을 동적으로 생성합니다.
+ private void BuildTopicButtons()
+ {
+ TopicButtonPanel.Children.Clear();
+ TopicButtonPanel.Visibility = Visibility.Visible;
+ if (TopicPresetScrollViewer != null)
+ TopicPresetScrollViewer.Visibility = Visibility.Visible;
+
+ if (_activeTab == "Cowork" || _activeTab == "Code")
+ {
+ if (EmptyStateTitle != null) EmptyStateTitle.Text = _activeTab == "Code"
+ ? "코드 작업을 입력하세요"
+ : "작업 유형을 선택하세요";
+ if (EmptyStateDesc != null) EmptyStateDesc.Text = _activeTab == "Code"
+ ? "코딩 에이전트가 코드 분석, 수정, 빌드, 테스트를 수행합니다"
+ : "에이전트가 상세한 데이터를 작성합니다";
+ }
+ else
+ {
+ if (EmptyStateTitle != null) EmptyStateTitle.Text = "대화 주제를 선택하세요";
+ if (EmptyStateDesc != null) EmptyStateDesc.Text = "주제에 맞는 전문 프리셋이 자동 적용됩니다";
+ }
+
+ if (_activeTab == "Code")
+ {
+ TopicButtonPanel.Visibility = Visibility.Collapsed;
+ if (TopicPresetScrollViewer != null)
+ TopicPresetScrollViewer.Visibility = Visibility.Collapsed;
+ return;
+ }
+
+ var presets = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets);
+ var cardBackground = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent;
+ var cardHoverBackground = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
+ var cardBorder = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E5E7EB");
+ var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
+ var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
+
+ void AttachTopicCardHover(Border card, Brush normalBackground, Brush hoverBackground)
+ {
+ card.MouseEnter += (sender, _) =>
+ {
+ if (sender is Border hovered)
+ {
+ hovered.Background = hoverBackground;
+ hovered.BorderBrush = TryFindResource("AccentColor") as Brush ?? cardBorder;
+ }
+ };
+ card.MouseLeave += (sender, _) =>
+ {
+ if (sender is Border hovered)
+ {
+ hovered.Background = normalBackground;
+ hovered.BorderBrush = cardBorder;
+ }
+ };
+ }
+
+ foreach (var preset in presets)
+ {
+ var capturedPreset = preset;
+ var buttonColor = BrushFromHex(preset.Color);
+
+ var border = new Border
+ {
+ Background = cardBackground,
+ BorderBrush = cardBorder,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(14),
+ Padding = new Thickness(14, 14, 14, 12),
+ Margin = new Thickness(6, 6, 6, 8),
+ Cursor = Cursors.Hand,
+ Width = 148,
+ Height = 124,
+ ClipToBounds = true,
+ };
+
+ var contentGrid = new Grid();
+ var stack = new StackPanel
+ {
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+
+ var iconCircle = new Border
+ {
+ Width = 34,
+ Height = 34,
+ CornerRadius = new CornerRadius(17),
+ Background = new SolidColorBrush(((SolidColorBrush)buttonColor).Color) { Opacity = 0.15 },
+ HorizontalAlignment = HorizontalAlignment.Center,
+ Margin = new Thickness(0, 0, 0, 9),
+ };
+ var iconBlock = new TextBlock
+ {
+ Text = preset.Symbol,
+ FontFamily = new FontFamily("Segoe MDL2 Assets"),
+ FontSize = 15,
+ Foreground = buttonColor,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ iconCircle.Child = iconBlock;
+ stack.Children.Add(iconCircle);
+
+ stack.Children.Add(new TextBlock
+ {
+ Text = preset.Label,
+ FontSize = 15,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = primaryText,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ TextAlignment = TextAlignment.Center,
+ TextWrapping = TextWrapping.Wrap,
+ MaxWidth = 112,
+ });
+
+ if (capturedPreset.IsCustom)
+ {
+ contentGrid.Children.Add(stack);
+ var badge = new Border
+ {
+ Width = 16,
+ Height = 16,
+ CornerRadius = new CornerRadius(4),
+ Background = new SolidColorBrush(Color.FromArgb(0x60, 0xFF, 0xFF, 0xFF)),
+ HorizontalAlignment = HorizontalAlignment.Left,
+ VerticalAlignment = VerticalAlignment.Top,
+ Margin = new Thickness(2, 2, 0, 0),
+ };
+ badge.Child = new TextBlock
+ {
+ Text = "\uE710",
+ FontFamily = new FontFamily("Segoe MDL2 Assets"),
+ FontSize = 8,
+ Foreground = buttonColor,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ contentGrid.Children.Add(badge);
+ }
+ else
+ {
+ contentGrid.Children.Add(stack);
+ }
+
+ border.Child = contentGrid;
+ AttachTopicCardHover(border, cardBackground, cardHoverBackground);
+ border.MouseLeftButtonDown += (_, _) => SelectTopic(capturedPreset);
+
+ if (capturedPreset.IsCustom)
+ {
+ border.MouseRightButtonUp += (sender, args) =>
+ {
+ args.Handled = true;
+ ShowCustomPresetContextMenu(sender as Border, capturedPreset);
+ };
+ }
+
+ TopicButtonPanel.Children.Add(border);
+ }
+
+ var etcColor = BrushFromHex("#6B7280");
+ var etcBorder = new Border
+ {
+ Background = cardBackground,
+ BorderBrush = cardBorder,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(14),
+ Padding = new Thickness(14, 14, 14, 12),
+ Margin = new Thickness(6, 6, 6, 8),
+ Cursor = Cursors.Hand,
+ Width = 148,
+ Height = 124,
+ ClipToBounds = true,
+ };
+
+ var etcGrid = new Grid();
+ var etcStack = new StackPanel
+ {
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+
+ var etcIconCircle = new Border
+ {
+ Width = 34,
+ Height = 34,
+ CornerRadius = new CornerRadius(17),
+ Background = new SolidColorBrush(((SolidColorBrush)etcColor).Color) { Opacity = 0.15 },
+ HorizontalAlignment = HorizontalAlignment.Center,
+ Margin = new Thickness(0, 0, 0, 9),
+ };
+ etcIconCircle.Child = new TextBlock
+ {
+ Text = "\uE70F",
+ FontFamily = new FontFamily("Segoe MDL2 Assets"),
+ FontSize = 15,
+ Foreground = etcColor,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ etcStack.Children.Add(etcIconCircle);
+
+ etcStack.Children.Add(new TextBlock
+ {
+ Text = "기타",
+ FontSize = 15,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = primaryText,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ TextAlignment = TextAlignment.Center,
+ });
+ etcGrid.Children.Add(etcStack);
+ etcBorder.Child = etcGrid;
+ AttachTopicCardHover(etcBorder, cardBackground, cardHoverBackground);
+ etcBorder.MouseLeftButtonDown += (_, _) =>
+ {
+ EmptyState.Visibility = Visibility.Collapsed;
+ InputBox.Focus();
+ };
+ TopicButtonPanel.Children.Add(etcBorder);
+
+ var addBorder = new Border
+ {
+ Background = Brushes.Transparent,
+ BorderBrush = cardBorder,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(14),
+ Padding = new Thickness(14, 14, 14, 12),
+ Margin = new Thickness(6, 6, 6, 8),
+ Cursor = Cursors.Hand,
+ Width = 148,
+ Height = 124,
+ ClipToBounds = true,
+ };
+
+ var addGrid = new Grid();
+ var addStack = new StackPanel
+ {
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+
+ addStack.Children.Add(new TextBlock
+ {
+ Text = "\uE710",
+ FontFamily = new FontFamily("Segoe MDL2 Assets"),
+ FontSize = 18,
+ Foreground = secondaryText,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ Margin = new Thickness(0, 8, 0, 8),
+ });
+ addStack.Children.Add(new TextBlock
+ {
+ Text = "프리셋 추가",
+ FontSize = 14,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = secondaryText,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ });
+
+ addGrid.Children.Add(addStack);
+ addBorder.Child = addGrid;
+ AttachTopicCardHover(addBorder, Brushes.Transparent, cardHoverBackground);
+ addBorder.MouseLeftButtonDown += (_, _) => ShowCustomPresetDialog();
+ TopicButtonPanel.Children.Add(addBorder);
+
+ UpdateTopicPresetScrollMode();
+ }
+
+ private void UpdateTopicPresetScrollMode()
+ {
+ if (TopicPresetScrollViewer == null || TopicButtonPanel == null)
+ return;
+
+ Dispatcher.BeginInvoke(new Action(() =>
+ {
+ if (TopicPresetScrollViewer == null || TopicButtonPanel == null)
+ return;
+
+ TopicPresetScrollViewer.UpdateLayout();
+ var shouldScroll = TopicPresetScrollViewer.ExtentHeight > TopicPresetScrollViewer.ViewportHeight + 1;
+ TopicPresetScrollViewer.VerticalScrollBarVisibility = shouldScroll
+ ? ScrollBarVisibility.Auto
+ : ScrollBarVisibility.Disabled;
+ TopicPresetScrollViewer.Padding = shouldScroll
+ ? new Thickness(0, 2, 6, 0)
+ : new Thickness(0, 2, 0, 0);
+ }), System.Windows.Threading.DispatcherPriority.Loaded);
+ }
+
+ private void ShowCustomPresetDialog(CustomPresetEntry? existing = null)
+ {
+ var dialog = new CustomPresetDialog(
+ existingName: existing?.Label ?? "",
+ existingDesc: existing?.Description ?? "",
+ existingPrompt: existing?.SystemPrompt ?? "",
+ existingColor: existing?.Color ?? "#6366F1",
+ existingSymbol: existing?.Symbol ?? "\uE713",
+ existingTab: existing?.Tab ?? _activeTab)
+ {
+ Owner = this,
+ };
+
+ if (dialog.ShowDialog() != true)
+ return;
+
+ if (existing != null)
+ {
+ existing.Label = dialog.PresetName;
+ existing.Description = dialog.PresetDescription;
+ existing.SystemPrompt = dialog.PresetSystemPrompt;
+ existing.Color = dialog.PresetColor;
+ existing.Symbol = dialog.PresetSymbol;
+ existing.Tab = dialog.PresetTab;
+ }
+ else
+ {
+ _settings.Settings.Llm.CustomPresets.Add(new CustomPresetEntry
+ {
+ Label = dialog.PresetName,
+ Description = dialog.PresetDescription,
+ SystemPrompt = dialog.PresetSystemPrompt,
+ Color = dialog.PresetColor,
+ Symbol = dialog.PresetSymbol,
+ Tab = dialog.PresetTab,
+ });
+ }
+
+ _settings.Save();
+ BuildTopicButtons();
+ }
+
+ private void ShowCustomPresetContextMenu(Border? anchor, Services.TopicPreset preset)
+ {
+ if (anchor == null || preset.CustomId == null)
+ return;
+
+ var popup = new Popup
+ {
+ PlacementTarget = anchor,
+ Placement = PlacementMode.Bottom,
+ StaysOpen = false,
+ AllowsTransparency = true,
+ };
+
+ var menuBackground = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black;
+ var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
+ var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
+ var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
+
+ var menuBorder = new Border
+ {
+ Background = menuBackground,
+ CornerRadius = new CornerRadius(10),
+ BorderBrush = borderBrush,
+ BorderThickness = new Thickness(1),
+ Padding = new Thickness(4),
+ MinWidth = 120,
+ Effect = new System.Windows.Media.Effects.DropShadowEffect
+ {
+ BlurRadius = 12,
+ ShadowDepth = 2,
+ Opacity = 0.3,
+ Color = Colors.Black,
+ },
+ };
+
+ var stack = new StackPanel();
+
+ var editItem = CreateContextMenuItem("\uE70F", "편집", primaryText);
+ editItem.MouseLeftButtonDown += (_, _) =>
+ {
+ popup.IsOpen = false;
+ var entry = _settings.Settings.Llm.CustomPresets.FirstOrDefault(item => item.Id == preset.CustomId);
+ if (entry != null)
+ ShowCustomPresetDialog(entry);
+ };
+ stack.Children.Add(editItem);
+
+ var deleteItem = CreateContextMenuItem("\uE74D", "삭제", new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)));
+ deleteItem.MouseLeftButtonDown += (_, _) =>
+ {
+ popup.IsOpen = false;
+ var result = CustomMessageBox.Show(
+ $"'{preset.Label}' 프리셋을 삭제하시겠습니까?",
+ "프리셋 삭제",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question);
+ if (result != MessageBoxResult.Yes)
+ return;
+
+ _settings.Settings.Llm.CustomPresets.RemoveAll(item => item.Id == preset.CustomId);
+ _settings.Save();
+ BuildTopicButtons();
+ };
+ stack.Children.Add(deleteItem);
+
+ menuBorder.Child = stack;
+ popup.Child = menuBorder;
+ popup.IsOpen = true;
+ }
+
+ private Border CreateContextMenuItem(string icon, string label, Brush foreground)
+ {
+ var item = new Border
+ {
+ Background = Brushes.Transparent,
+ CornerRadius = new CornerRadius(6),
+ Padding = new Thickness(10, 6, 14, 6),
+ Cursor = Cursors.Hand,
+ };
+
+ var stack = new StackPanel { Orientation = Orientation.Horizontal };
+ stack.Children.Add(new TextBlock
+ {
+ Text = icon,
+ FontFamily = new FontFamily("Segoe MDL2 Assets"),
+ FontSize = 13,
+ Foreground = foreground,
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(0, 0, 8, 0),
+ });
+ stack.Children.Add(new TextBlock
+ {
+ Text = label,
+ FontSize = 13,
+ Foreground = foreground,
+ VerticalAlignment = VerticalAlignment.Center,
+ });
+ item.Child = stack;
+
+ var hoverBackground = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
+ item.MouseEnter += (sender, _) =>
+ {
+ if (sender is Border hovered)
+ hovered.Background = hoverBackground;
+ };
+ item.MouseLeave += (sender, _) =>
+ {
+ if (sender is Border hovered)
+ hovered.Background = Brushes.Transparent;
+ };
+
+ return item;
+ }
+
+ private Border CreateContextMenuItem(string icon, string label, Brush foreground, Brush secondaryForeground)
+ => CreateContextMenuItem(icon, label, foreground);
+
+ private void SelectTopic(Services.TopicPreset preset)
+ {
+ bool hasConversation;
+ bool hasMessages;
+ lock (_convLock)
+ {
+ hasConversation = _currentConversation != null;
+ hasMessages = _currentConversation?.Messages.Count > 0;
+ }
+
+ var hasInput = !string.IsNullOrEmpty(InputBox.Text);
+ if (!hasConversation)
+ StartNewConversation();
+
+ lock (_convLock)
+ {
+ if (_currentConversation == null)
+ return;
+
+ var session = ChatSession;
+ if (session != null)
+ {
+ _currentConversation = session.UpdateConversationMetadata(_activeTab, conversation =>
+ {
+ conversation.SystemCommand = preset.SystemPrompt;
+ conversation.Category = preset.Category;
+ }, _storage);
+ }
+ else
+ {
+ _currentConversation.SystemCommand = preset.SystemPrompt;
+ _currentConversation.Category = preset.Category;
+ }
+ }
+
+ UpdateCategoryLabel();
+ SaveConversationSettings();
+ RefreshConversationList();
+ UpdateSelectedPresetGuide();
+ if (EmptyState != null)
+ EmptyState.Visibility = Visibility.Collapsed;
+
+ InputBox.Focus();
+
+ if (!string.IsNullOrEmpty(preset.Placeholder))
+ {
+ _promptCardPlaceholder = preset.Placeholder;
+ if (!hasMessages && !hasInput)
+ ShowPlaceholder();
+ }
+
+ if (hasMessages || hasInput)
+ ShowToast($"프리셋 변경: {preset.Label}");
+
+ if (_activeTab == "Cowork")
+ BuildBottomBar();
+ }
+}
diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs
index 8960b7b..79024ff 100644
--- a/src/AxCopilot/Views/ChatWindow.xaml.cs
+++ b/src/AxCopilot/Views/ChatWindow.xaml.cs
@@ -10110,521 +10110,6 @@ public partial class ChatWindow : Window
_toastHideTimer.Start();
}
- // ─── 대화 주제 버튼 ──────────────────────────────────────────────────
-
- /// 프리셋에서 대화 주제 버튼을 동적으로 생성합니다.
- private void BuildTopicButtons()
- {
- TopicButtonPanel.Children.Clear();
- TopicButtonPanel.Visibility = Visibility.Visible;
- if (TopicPresetScrollViewer != null)
- TopicPresetScrollViewer.Visibility = Visibility.Visible;
-
- // 탭별 EmptyState 텍스트
- if (_activeTab == "Cowork" || _activeTab == "Code")
- {
- if (EmptyStateTitle != null) EmptyStateTitle.Text = _activeTab == "Code"
- ? "코드 작업을 입력하세요"
- : "작업 유형을 선택하세요";
- if (EmptyStateDesc != null) EmptyStateDesc.Text = _activeTab == "Code"
- ? "코딩 에이전트가 코드 분석, 수정, 빌드, 테스트를 수행합니다"
- : "에이전트가 상세한 데이터를 작성합니다";
- }
- else
- {
- if (EmptyStateTitle != null) EmptyStateTitle.Text = "대화 주제를 선택하세요";
- if (EmptyStateDesc != null) EmptyStateDesc.Text = "주제에 맞는 전문 프리셋이 자동 적용됩니다";
- }
-
- if (_activeTab == "Code")
- {
- TopicButtonPanel.Visibility = Visibility.Collapsed;
- if (TopicPresetScrollViewer != null)
- TopicPresetScrollViewer.Visibility = Visibility.Collapsed;
- return;
- }
-
- var presets = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets);
- var cardBackground = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent;
- var cardHoverBackground = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
- var cardBorder = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E5E7EB");
- var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
- var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
-
- void AttachTopicCardHover(Border card, Brush normalBackground, Brush hoverBackground)
- {
- card.MouseEnter += (s, _) =>
- {
- if (s is Border b)
- {
- b.Background = hoverBackground;
- b.BorderBrush = TryFindResource("AccentColor") as Brush ?? cardBorder;
- }
- };
- card.MouseLeave += (s, _) =>
- {
- if (s is Border b)
- {
- b.Background = normalBackground;
- b.BorderBrush = cardBorder;
- }
- };
- }
-
- foreach (var preset in presets)
- {
- var capturedPreset = preset;
- var btnColor = BrushFromHex(preset.Color);
-
- var border = new Border
- {
- Background = cardBackground,
- BorderBrush = cardBorder,
- BorderThickness = new Thickness(1),
- CornerRadius = new CornerRadius(14),
- Padding = new Thickness(14, 14, 14, 12),
- Margin = new Thickness(6, 6, 6, 8),
- Cursor = Cursors.Hand,
- Width = 148,
- Height = 124,
- ClipToBounds = true,
- };
-
- var contentGrid = new Grid();
- var stack = new StackPanel
- {
- HorizontalAlignment = HorizontalAlignment.Center,
- VerticalAlignment = VerticalAlignment.Center,
- };
-
- var iconCircle = new Border
- {
- Width = 34, Height = 34,
- CornerRadius = new CornerRadius(17),
- Background = new SolidColorBrush(((SolidColorBrush)btnColor).Color) { Opacity = 0.15 },
- HorizontalAlignment = HorizontalAlignment.Center,
- Margin = new Thickness(0, 0, 0, 9),
- };
- var iconTb = new TextBlock
- {
- Text = preset.Symbol,
- FontFamily = new FontFamily("Segoe MDL2 Assets"),
- FontSize = 15,
- Foreground = btnColor,
- HorizontalAlignment = HorizontalAlignment.Center,
- VerticalAlignment = VerticalAlignment.Center,
- };
- iconCircle.Child = iconTb;
- stack.Children.Add(iconCircle);
-
- stack.Children.Add(new TextBlock
- {
- Text = preset.Label,
- FontSize = 15,
- FontWeight = FontWeights.SemiBold,
- Foreground = primaryText,
- HorizontalAlignment = HorizontalAlignment.Center,
- TextAlignment = TextAlignment.Center,
- TextWrapping = TextWrapping.Wrap,
- MaxWidth = 112,
- });
-
- // 커스텀 프리셋: 좌측 상단 뱃지
- if (capturedPreset.IsCustom)
- {
- contentGrid.Children.Add(stack);
- var badge = new Border
- {
- Width = 16, Height = 16,
- CornerRadius = new CornerRadius(4),
- Background = new SolidColorBrush(Color.FromArgb(0x60, 0xFF, 0xFF, 0xFF)),
- HorizontalAlignment = HorizontalAlignment.Left,
- VerticalAlignment = VerticalAlignment.Top,
- Margin = new Thickness(2, 2, 0, 0),
- };
- badge.Child = new TextBlock
- {
- Text = "\uE710", // + 아이콘
- FontFamily = new FontFamily("Segoe MDL2 Assets"),
- FontSize = 8,
- Foreground = btnColor,
- HorizontalAlignment = HorizontalAlignment.Center,
- VerticalAlignment = VerticalAlignment.Center,
- };
- contentGrid.Children.Add(badge);
- }
- else
- {
- contentGrid.Children.Add(stack);
- }
-
- border.Child = contentGrid;
- AttachTopicCardHover(border, cardBackground, cardHoverBackground);
- // 클릭 → 해당 주제로 새 대화 시작
- border.MouseLeftButtonDown += (_, _) => SelectTopic(capturedPreset);
-
- // 커스텀 프리셋: 우클릭 메뉴 (편집/삭제)
- if (capturedPreset.IsCustom)
- {
- border.MouseRightButtonUp += (s, e) =>
- {
- e.Handled = true;
- ShowCustomPresetContextMenu(s as Border, capturedPreset);
- };
- }
-
- TopicButtonPanel.Children.Add(border);
- }
-
- // "기타" 자유 입력 버튼 추가
- {
- var etcColor = BrushFromHex("#6B7280"); // 회색
- var etcBorder = new Border
- {
- Background = cardBackground,
- BorderBrush = cardBorder,
- BorderThickness = new Thickness(1),
- CornerRadius = new CornerRadius(14),
- Padding = new Thickness(14, 14, 14, 12),
- Margin = new Thickness(6, 6, 6, 8),
- Cursor = Cursors.Hand,
- Width = 148,
- Height = 124,
- ClipToBounds = true,
- };
-
- var etcGrid = new Grid();
- var etcStack = new StackPanel
- {
- HorizontalAlignment = HorizontalAlignment.Center,
- VerticalAlignment = VerticalAlignment.Center,
- };
-
- var etcIconCircle = new Border
- {
- Width = 34, Height = 34,
- CornerRadius = new CornerRadius(17),
- Background = new SolidColorBrush(((SolidColorBrush)etcColor).Color) { Opacity = 0.15 },
- HorizontalAlignment = HorizontalAlignment.Center,
- Margin = new Thickness(0, 0, 0, 9),
- };
- etcIconCircle.Child = new TextBlock
- {
- Text = "\uE70F", // Edit 아이콘
- FontFamily = new FontFamily("Segoe MDL2 Assets"),
- FontSize = 15,
- Foreground = etcColor,
- HorizontalAlignment = HorizontalAlignment.Center,
- VerticalAlignment = VerticalAlignment.Center,
- };
- etcStack.Children.Add(etcIconCircle);
-
- etcStack.Children.Add(new TextBlock
- {
- Text = "기타",
- FontSize = 15,
- FontWeight = FontWeights.SemiBold,
- Foreground = primaryText,
- HorizontalAlignment = HorizontalAlignment.Center,
- TextAlignment = TextAlignment.Center,
- });
- etcGrid.Children.Add(etcStack);
- etcBorder.Child = etcGrid;
- AttachTopicCardHover(etcBorder, cardBackground, cardHoverBackground);
- etcBorder.MouseLeftButtonDown += (_, _) =>
- {
- EmptyState.Visibility = Visibility.Collapsed;
- InputBox.Focus();
- };
- TopicButtonPanel.Children.Add(etcBorder);
- }
-
- // ── "+" 커스텀 프리셋 추가 버튼 ──
- {
- var addColor = BrushFromHex("#6366F1");
- var addBorder = new Border
- {
- Background = Brushes.Transparent,
- BorderBrush = cardBorder,
- BorderThickness = new Thickness(1),
- CornerRadius = new CornerRadius(14),
- Padding = new Thickness(14, 14, 14, 12),
- Margin = new Thickness(6, 6, 6, 8),
- Cursor = Cursors.Hand,
- Width = 148,
- Height = 124,
- ClipToBounds = true,
- };
-
- var addGrid = new Grid();
- var addStack = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center };
-
- // + 아이콘
- var plusIcon = new TextBlock
- {
- Text = "\uE710",
- FontFamily = new FontFamily("Segoe MDL2 Assets"),
- FontSize = 18,
- Foreground = secondaryText,
- HorizontalAlignment = HorizontalAlignment.Center,
- Margin = new Thickness(0, 8, 0, 8),
- };
- addStack.Children.Add(plusIcon);
-
- addStack.Children.Add(new TextBlock
- {
- Text = "프리셋 추가",
- FontSize = 14,
- FontWeight = FontWeights.SemiBold,
- Foreground = secondaryText,
- HorizontalAlignment = HorizontalAlignment.Center,
- });
-
- addGrid.Children.Add(addStack);
- addBorder.Child = addGrid;
- AttachTopicCardHover(addBorder, Brushes.Transparent, cardHoverBackground);
- addBorder.MouseLeftButtonDown += (_, _) => ShowCustomPresetDialog();
- TopicButtonPanel.Children.Add(addBorder);
- }
-
- UpdateTopicPresetScrollMode();
- }
-
- private void UpdateTopicPresetScrollMode()
- {
- if (TopicPresetScrollViewer == null || TopicButtonPanel == null)
- return;
-
- Dispatcher.BeginInvoke(new Action(() =>
- {
- if (TopicPresetScrollViewer == null || TopicButtonPanel == null)
- return;
-
- TopicPresetScrollViewer.UpdateLayout();
- var shouldScroll = TopicPresetScrollViewer.ExtentHeight > TopicPresetScrollViewer.ViewportHeight + 1;
- TopicPresetScrollViewer.VerticalScrollBarVisibility = shouldScroll
- ? ScrollBarVisibility.Auto
- : ScrollBarVisibility.Disabled;
- TopicPresetScrollViewer.Padding = shouldScroll
- ? new Thickness(0, 2, 6, 0)
- : new Thickness(0, 2, 0, 0);
- }), System.Windows.Threading.DispatcherPriority.Loaded);
- }
-
- // ─── 커스텀 프리셋 관리 ─────────────────────────────────────────────
-
- /// 커스텀 프리셋 추가 다이얼로그를 표시합니다.
- private void ShowCustomPresetDialog(Models.CustomPresetEntry? existing = null)
- {
- bool isEdit = existing != null;
- var dlg = new CustomPresetDialog(
- existingName: existing?.Label ?? "",
- existingDesc: existing?.Description ?? "",
- existingPrompt: existing?.SystemPrompt ?? "",
- existingColor: existing?.Color ?? "#6366F1",
- existingSymbol: existing?.Symbol ?? "\uE713",
- existingTab: existing?.Tab ?? _activeTab)
- {
- Owner = this,
- };
-
- if (dlg.ShowDialog() == true)
- {
- if (isEdit)
- {
- existing!.Label = dlg.PresetName;
- existing.Description = dlg.PresetDescription;
- existing.SystemPrompt = dlg.PresetSystemPrompt;
- existing.Color = dlg.PresetColor;
- existing.Symbol = dlg.PresetSymbol;
- existing.Tab = dlg.PresetTab;
- }
- else
- {
- _settings.Settings.Llm.CustomPresets.Add(new Models.CustomPresetEntry
- {
- Label = dlg.PresetName,
- Description = dlg.PresetDescription,
- SystemPrompt = dlg.PresetSystemPrompt,
- Color = dlg.PresetColor,
- Symbol = dlg.PresetSymbol,
- Tab = dlg.PresetTab,
- });
- }
- _settings.Save();
- BuildTopicButtons();
- }
- }
-
- /// 커스텀 프리셋 우클릭 컨텍스트 메뉴를 표시합니다.
- private void ShowCustomPresetContextMenu(Border? anchor, Services.TopicPreset preset)
- {
- if (anchor == null || preset.CustomId == null) return;
-
- var popup = new System.Windows.Controls.Primitives.Popup
- {
- PlacementTarget = anchor,
- Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom,
- StaysOpen = false,
- AllowsTransparency = true,
- };
-
- var menuBg = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black;
- var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
- var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
- var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
-
- var menuBorder = new Border
- {
- Background = menuBg,
- CornerRadius = new CornerRadius(10),
- BorderBrush = borderBrush,
- BorderThickness = new Thickness(1),
- Padding = new Thickness(4),
- MinWidth = 120,
- Effect = new System.Windows.Media.Effects.DropShadowEffect
- {
- BlurRadius = 12, ShadowDepth = 2, Opacity = 0.3, Color = Colors.Black,
- },
- };
-
- var stack = new StackPanel();
-
- // 편집 버튼
- var editItem = CreateContextMenuItem("\uE70F", "편집", primaryText, secondaryText);
- editItem.MouseLeftButtonDown += (_, _) =>
- {
- popup.IsOpen = false;
- var entry = _settings.Settings.Llm.CustomPresets.FirstOrDefault(c => c.Id == preset.CustomId);
- if (entry != null) ShowCustomPresetDialog(entry);
- };
- stack.Children.Add(editItem);
-
- // 삭제 버튼
- var deleteItem = CreateContextMenuItem("\uE74D", "삭제", new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)), secondaryText);
- deleteItem.MouseLeftButtonDown += (_, _) =>
- {
- popup.IsOpen = false;
- var result = CustomMessageBox.Show(
- $"'{preset.Label}' 프리셋을 삭제하시겠습니까?",
- "프리셋 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question);
- if (result == MessageBoxResult.Yes)
- {
- _settings.Settings.Llm.CustomPresets.RemoveAll(c => c.Id == preset.CustomId);
- _settings.Save();
- BuildTopicButtons();
- }
- };
- stack.Children.Add(deleteItem);
-
- menuBorder.Child = stack;
- popup.Child = menuBorder;
- popup.IsOpen = true;
- }
-
- /// 컨텍스트 메뉴 항목을 생성합니다.
- private Border CreateContextMenuItem(string icon, string label, Brush fg, Brush secondaryFg)
- {
- var item = new Border
- {
- Background = Brushes.Transparent,
- CornerRadius = new CornerRadius(6),
- Padding = new Thickness(10, 6, 14, 6),
- Cursor = Cursors.Hand,
- };
-
- var sp = new StackPanel { Orientation = Orientation.Horizontal };
- sp.Children.Add(new TextBlock
- {
- Text = icon,
- FontFamily = new FontFamily("Segoe MDL2 Assets"),
- FontSize = 13, Foreground = fg,
- VerticalAlignment = VerticalAlignment.Center,
- Margin = new Thickness(0, 0, 8, 0),
- });
- sp.Children.Add(new TextBlock
- {
- Text = label, FontSize = 13, Foreground = fg,
- VerticalAlignment = VerticalAlignment.Center,
- });
- item.Child = sp;
-
- var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
- item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
- item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
-
- return item;
- }
-
- /// 대화 주제 선택 — 프리셋 시스템 프롬프트 + 카테고리 적용.
- private void SelectTopic(Services.TopicPreset preset)
- {
- bool hasMessages;
- bool hasConversation;
- lock (_convLock)
- {
- hasConversation = _currentConversation != null;
- hasMessages = _currentConversation?.Messages.Count > 0;
- }
-
- // 입력란에 텍스트가 있으면 기존 대화를 유지 (입력 내용 보존)
- bool hasInput = !string.IsNullOrEmpty(InputBox.Text);
- bool keepConversation = hasConversation;
-
- if (!keepConversation)
- {
- // 현재 대화가 아예 없는 경우에만 새 대화 시작
- StartNewConversation();
- keepConversation = true;
- }
-
- // 프리셋 적용 (기존 대화에도 프리셋 변경 가능)
- lock (_convLock)
- {
- if (_currentConversation != null)
- {
- var session = ChatSession;
- if (session != null)
- {
- _currentConversation = session.UpdateConversationMetadata(_activeTab, c =>
- {
- c.SystemCommand = preset.SystemPrompt;
- c.Category = preset.Category;
- }, _storage);
- }
- else
- {
- _currentConversation.SystemCommand = preset.SystemPrompt;
- _currentConversation.Category = preset.Category;
- }
- }
- }
-
- UpdateCategoryLabel();
- SaveConversationSettings();
- RefreshConversationList();
- UpdateSelectedPresetGuide();
- if (EmptyState != null)
- EmptyState.Visibility = Visibility.Collapsed;
-
- InputBox.Focus();
-
- if (!string.IsNullOrEmpty(preset.Placeholder))
- {
- _promptCardPlaceholder = preset.Placeholder;
- if (!hasMessages && !hasInput) ShowPlaceholder();
- }
-
- if (hasMessages || hasInput)
- ShowToast($"프리셋 변경: {preset.Label}");
-
- // Cowork 탭: 하단 바 갱신
- if (_activeTab == "Cowork")
- BuildBottomBar();
- }
-
-
-
/// 선택된 디자인 무드 키 (HtmlSkill에서 사용).
private string _selectedMood = null!; // Loaded 이벤트에서 초기화
private string _selectedLanguage = "auto"; // Code 탭 개발 언어