diff --git a/README.md b/README.md
index 10ff94b..c95f90a 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,10 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
개발 참고: Claw Code 동등성 작업 추적 문서
`docs/claw-code-parity-plan.md`
+- 업데이트: 2026-04-06 09:03 (KST)
+- AX Agent 공통 선택 팝업 조립 로직을 `ChatWindow.PopupPresentation.cs`로 분리했습니다. 테마 팝업 컨테이너, 공통 메뉴 아이템, 구분선, 최근 폴더 우클릭 컨텍스트 메뉴가 메인 창 코드 밖으로 이동해 footer/file-browser 쪽 팝업 품질 작업을 이어가기 쉬운 구조로 정리했습니다.
+- `ChatWindow.xaml.cs`는 대화 상태와 런타임 orchestration 쪽에 더 집중하도록 정리했고, 공통 팝업 시각 언어를 한 곳에서 다듬을 수 있는 기반을 만들었습니다.
+
- 업데이트: 2026-04-06 08:55 (KST)
- AX Agent 파일 브라우저 렌더를 `ChatWindow.FileBrowserPresentation.cs`로 분리했습니다. 파일 탐색기 열기/닫기, 폴더 트리 구성, 파일 헤더/아이콘/크기 표시, 우클릭 메뉴, 디바운스 새로고침 흐름이 메인 창 코드 밖으로 이동했습니다.
- `ChatWindow.xaml.cs`는 transcript·runtime orchestration 중심으로 더 정리됐고, claw-code 기준 사이드 surface 품질 작업을 이어가기 쉬운 구조로 맞췄습니다.
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 6162af7..984273e 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -1,5 +1,8 @@
# AX Copilot - 媛쒕컻 臾몄꽌
+- Document update: 2026-04-06 09:03 (KST) - Split common themed popup construction out of `ChatWindow.xaml.cs` into `ChatWindow.PopupPresentation.cs`. Shared popup container creation, generic popup menu items/separators, and the recent-folder context menu now live in a dedicated partial instead of the main window orchestration file.
+- Document update: 2026-04-06 09:03 (KST) - This keeps footer/file-browser popup styling on a single visual path and reduces direct popup composition inside the main chat window flow, making further `claw-code` style popup UX work easier to maintain.
+
- Document update: 2026-04-06 07:31 (KST) - Split permission presentation logic out of `ChatWindow.xaml.cs` into `ChatWindow.PermissionPresentation.cs`. Permission popup row construction, popup refresh, section expansion persistence, and permission banner/status styling now live in a dedicated partial instead of the main window orchestration file.
- Document update: 2026-04-06 07:31 (KST) - Split context usage card/popup rendering into `ChatWindow.ContextUsagePresentation.cs`. The Cowork/Code context usage ring, tooltip popup copy, hover close behavior, and screen-coordinate hit testing are now isolated from the rest of the chat window flow.
- Document update: 2026-04-06 01:37 (KST) - Reworked AX Agent plan approval toward a more transcript-native flow. The inline decision card remains the primary approval path, while the `계획` affordance now opens the stored plan as a detail-only surface instead of acting like a required popup step.
diff --git a/src/AxCopilot/Views/ChatWindow.PopupPresentation.cs b/src/AxCopilot/Views/ChatWindow.PopupPresentation.cs
new file mode 100644
index 0000000..f15fcd5
--- /dev/null
+++ b/src/AxCopilot/Views/ChatWindow.PopupPresentation.cs
@@ -0,0 +1,159 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Threading;
+
+namespace AxCopilot.Views;
+
+public partial class ChatWindow
+{
+ private Popup? _sharedContextPopup;
+
+ private (Popup Popup, StackPanel Panel) CreateThemedPopupMenu(
+ UIElement? placementTarget = null,
+ PlacementMode placement = PlacementMode.MousePoint,
+ double minWidth = 200)
+ {
+ _sharedContextPopup?.SetCurrentValue(Popup.IsOpenProperty, false);
+
+ var bg = TryFindResource("LauncherBackground") as Brush ?? Brushes.White;
+ var border = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
+ var panel = new StackPanel { Margin = new Thickness(2) };
+ var container = new Border
+ {
+ Background = bg,
+ BorderBrush = border,
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(10),
+ Padding = new Thickness(6),
+ MinWidth = minWidth,
+ Child = panel,
+ Effect = new System.Windows.Media.Effects.DropShadowEffect
+ {
+ BlurRadius = 16,
+ ShadowDepth = 3,
+ Opacity = 0.18,
+ Color = Colors.Black,
+ Direction = 270,
+ },
+ };
+
+ var popup = new Popup
+ {
+ Child = container,
+ StaysOpen = false,
+ AllowsTransparency = true,
+ PopupAnimation = PopupAnimation.Fade,
+ Placement = placement,
+ PlacementTarget = placementTarget,
+ };
+
+ _sharedContextPopup = popup;
+ return (popup, panel);
+ }
+
+ private Border CreatePopupMenuItem(
+ Popup popup,
+ string icon,
+ string label,
+ Brush iconBrush,
+ Brush labelBrush,
+ Brush hoverBrush,
+ Action action)
+ {
+ var sp = new StackPanel { Orientation = Orientation.Horizontal };
+ sp.Children.Add(new TextBlock
+ {
+ Text = icon,
+ FontFamily = new FontFamily("Segoe MDL2 Assets"),
+ FontSize = 12.5,
+ Foreground = iconBrush,
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(0, 0, 9, 0),
+ });
+ sp.Children.Add(new TextBlock
+ {
+ Text = label,
+ FontSize = 12.5,
+ Foreground = labelBrush,
+ VerticalAlignment = VerticalAlignment.Center,
+ });
+
+ var item = new Border
+ {
+ Child = sp,
+ Background = Brushes.Transparent,
+ CornerRadius = new CornerRadius(8),
+ Cursor = Cursors.Hand,
+ Padding = new Thickness(10, 7, 12, 7),
+ Margin = new Thickness(0, 1, 0, 1),
+ };
+ item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBrush; };
+ item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
+ item.MouseLeftButtonUp += (_, _) =>
+ {
+ popup.SetCurrentValue(Popup.IsOpenProperty, false);
+ action();
+ };
+
+ return item;
+ }
+
+ private static void AddPopupMenuSeparator(Panel panel, Brush brush)
+ {
+ panel.Children.Add(new Border
+ {
+ Height = 1,
+ Margin = new Thickness(10, 4, 10, 4),
+ Background = brush,
+ Opacity = 0.35,
+ });
+ }
+
+ /// 최근 폴더 항목 우클릭 컨텍스트 메뉴를 표시합니다.
+ private void ShowRecentFolderContextMenu(string folderPath)
+ {
+ 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 hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
+ var warningBrush = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44));
+ var (popup, panel) = CreateThemedPopupMenu();
+
+ panel.Children.Add(CreatePopupMenuItem(popup, "\uED25", "폴더 열기", secondaryText, primaryText, hoverBg, () =>
+ {
+ try
+ {
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = folderPath,
+ UseShellExecute = true,
+ });
+ }
+ catch
+ {
+ }
+ }));
+
+ panel.Children.Add(CreatePopupMenuItem(popup, "\uE8C8", "경로 복사", secondaryText, primaryText, hoverBg, () =>
+ {
+ try { Clipboard.SetText(folderPath); } catch { }
+ }));
+
+ AddPopupMenuSeparator(panel, borderBrush);
+
+ panel.Children.Add(CreatePopupMenuItem(popup, "\uE74D", "목록에서 삭제", warningBrush, warningBrush, hoverBg, () =>
+ {
+ _settings.Settings.Llm.RecentWorkFolders.RemoveAll(
+ p => p.Equals(folderPath, StringComparison.OrdinalIgnoreCase));
+ _settings.Save();
+ if (FolderMenuPopup.IsOpen)
+ ShowFolderMenu();
+ }));
+
+ Dispatcher.BeginInvoke(() => { popup.IsOpen = true; }, DispatcherPriority.Input);
+ }
+}
diff --git a/src/AxCopilot/Views/ChatWindow.VisualInteractionHelpers.cs b/src/AxCopilot/Views/ChatWindow.VisualInteractionHelpers.cs
new file mode 100644
index 0000000..37e4a8d
--- /dev/null
+++ b/src/AxCopilot/Views/ChatWindow.VisualInteractionHelpers.cs
@@ -0,0 +1,259 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+
+namespace AxCopilot.Views;
+
+public partial class ChatWindow
+{
+ /// 마우스 오버 시 살짝 확대하는 호버 애니메이션. 독립적 공간이 있는 버튼에만 적용합니다.
+ private static void ApplyHoverScaleAnimation(FrameworkElement element, double hoverScale = 1.08)
+ {
+ void EnsureTransform()
+ {
+ element.RenderTransformOrigin = new Point(0.5, 0.5);
+ if (element.RenderTransform is not ScaleTransform || element.RenderTransform.IsFrozen)
+ element.RenderTransform = new ScaleTransform(1, 1);
+ }
+
+ element.Loaded += (_, _) => EnsureTransform();
+
+ element.MouseEnter += (_, _) =>
+ {
+ EnsureTransform();
+ var st = (ScaleTransform)element.RenderTransform;
+ var grow = new DoubleAnimation(hoverScale, TimeSpan.FromMilliseconds(150))
+ {
+ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
+ };
+ st.BeginAnimation(ScaleTransform.ScaleXProperty, grow);
+ st.BeginAnimation(ScaleTransform.ScaleYProperty, grow);
+ };
+
+ element.MouseLeave += (_, _) =>
+ {
+ EnsureTransform();
+ var st = (ScaleTransform)element.RenderTransform;
+ var shrink = new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(200))
+ {
+ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
+ };
+ st.BeginAnimation(ScaleTransform.ScaleXProperty, shrink);
+ st.BeginAnimation(ScaleTransform.ScaleYProperty, shrink);
+ };
+ }
+
+ /// 마우스 오버 시 텍스트가 살짝 튀어오르는 바운스 애니메이션.
+ private static void ApplyHoverBounceAnimation(FrameworkElement element, double bounceY = -2.5)
+ {
+ void EnsureTransform()
+ {
+ if (element.RenderTransform is not TranslateTransform || element.RenderTransform.IsFrozen)
+ element.RenderTransform = new TranslateTransform(0, 0);
+ }
+
+ element.Loaded += (_, _) => EnsureTransform();
+
+ element.MouseEnter += (_, _) =>
+ {
+ EnsureTransform();
+ var tt = (TranslateTransform)element.RenderTransform;
+ tt.BeginAnimation(
+ TranslateTransform.YProperty,
+ new DoubleAnimation(bounceY, TimeSpan.FromMilliseconds(200))
+ {
+ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
+ });
+ };
+
+ element.MouseLeave += (_, _) =>
+ {
+ EnsureTransform();
+ var tt = (TranslateTransform)element.RenderTransform;
+ tt.BeginAnimation(
+ TranslateTransform.YProperty,
+ new DoubleAnimation(0, TimeSpan.FromMilliseconds(250))
+ {
+ EasingFunction = new ElasticEase
+ {
+ EasingMode = EasingMode.EaseOut,
+ Oscillations = 1,
+ Springiness = 10
+ }
+ });
+ };
+ }
+
+ /// 심플한 V 체크 아이콘을 생성합니다.
+ private static FrameworkElement CreateSimpleCheck(Brush color, double size = 14)
+ {
+ return new System.Windows.Shapes.Path
+ {
+ Data = Geometry.Parse($"M {size * 0.15} {size * 0.5} L {size * 0.4} {size * 0.75} L {size * 0.85} {size * 0.28}"),
+ Stroke = color,
+ StrokeThickness = 2,
+ StrokeStartLineCap = PenLineCap.Round,
+ StrokeEndLineCap = PenLineCap.Round,
+ StrokeLineJoin = PenLineJoin.Round,
+ Width = size,
+ Height = size,
+ Margin = new Thickness(0, 0, 10, 0),
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ }
+
+ /// 팝업 메뉴 항목에 호버 배경색 + 미세 확대 효과를 적용합니다.
+ private static void ApplyMenuItemHover(Border item)
+ {
+ var originalBg = item.Background?.Clone() ?? Brushes.Transparent;
+ if (originalBg.CanFreeze)
+ originalBg.Freeze();
+
+ item.RenderTransformOrigin = new Point(0.5, 0.5);
+ item.RenderTransform = new ScaleTransform(1, 1);
+
+ item.MouseEnter += (s, _) =>
+ {
+ if (s is Border b)
+ {
+ if (originalBg is SolidColorBrush scb && scb.Color.A > 0x20)
+ b.Opacity = 0.85;
+ else
+ b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
+ }
+
+ var st = item.RenderTransform as ScaleTransform;
+ st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
+ st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
+ };
+
+ item.MouseLeave += (s, _) =>
+ {
+ if (s is Border b)
+ {
+ b.Opacity = 1.0;
+ b.Background = originalBg;
+ }
+
+ var st = item.RenderTransform as ScaleTransform;
+ st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
+ st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
+ };
+ }
+
+ private Button CreateActionButton(string symbol, string tooltip, Brush foreground, Action onClick)
+ {
+ var hoverBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
+ var hoverBg = TryFindResource("ItemHoverBackground") as Brush
+ ?? new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF));
+ var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
+ var icon = new TextBlock
+ {
+ Text = symbol,
+ FontFamily = new FontFamily("Segoe MDL2 Assets"),
+ FontSize = 10,
+ Foreground = foreground,
+ VerticalAlignment = VerticalAlignment.Center
+ };
+
+ var btn = new Button
+ {
+ Content = icon,
+ Background = Brushes.Transparent,
+ BorderBrush = borderBrush,
+ BorderThickness = new Thickness(1),
+ Cursor = Cursors.Hand,
+ Width = 24,
+ Height = 24,
+ Padding = new Thickness(0),
+ Margin = new Thickness(0, 0, 2, 0),
+ ToolTip = tooltip
+ };
+
+ btn.Template = BuildMinimalIconButtonTemplate();
+ btn.MouseEnter += (_, _) =>
+ {
+ icon.Foreground = hoverBrush;
+ btn.Background = hoverBg;
+ };
+ btn.MouseLeave += (_, _) =>
+ {
+ icon.Foreground = foreground;
+ btn.Background = Brushes.Transparent;
+ };
+ btn.Click += (_, _) => onClick();
+ return btn;
+ }
+
+ private void ShowMessageActionBar(StackPanel actionBar)
+ {
+ if (actionBar == null)
+ return;
+
+ actionBar.Opacity = 1;
+ }
+
+ private void HideMessageActionBarIfNotSelected(StackPanel actionBar)
+ {
+ if (actionBar == null)
+ return;
+
+ if (!ReferenceEquals(_selectedMessageActionBar, actionBar))
+ actionBar.Opacity = 0;
+ }
+
+ private void SelectMessageActionBar(StackPanel actionBar, Border? messageBorder = null)
+ {
+ if (_selectedMessageActionBar != null && !ReferenceEquals(_selectedMessageActionBar, actionBar))
+ _selectedMessageActionBar.Opacity = 0;
+
+ if (_selectedMessageBorder != null && !ReferenceEquals(_selectedMessageBorder, messageBorder))
+ ApplyMessageSelectionStyle(_selectedMessageBorder, false);
+
+ _selectedMessageActionBar = actionBar;
+ _selectedMessageActionBar.Opacity = 1;
+ _selectedMessageBorder = messageBorder;
+ if (_selectedMessageBorder != null)
+ ApplyMessageSelectionStyle(_selectedMessageBorder, true);
+ }
+
+ private void ApplyMessageSelectionStyle(Border border, bool selected)
+ {
+ if (border == null)
+ return;
+
+ var accent = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
+ var defaultBorder = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
+ border.BorderBrush = selected ? accent : defaultBorder;
+ border.BorderThickness = selected ? new Thickness(1.5) : new Thickness(1);
+ border.Effect = selected
+ ? new System.Windows.Media.Effects.DropShadowEffect
+ {
+ BlurRadius = 16,
+ ShadowDepth = 0,
+ Opacity = 0.10,
+ Color = Colors.Black,
+ }
+ : null;
+ }
+
+ private static ControlTemplate BuildMinimalIconButtonTemplate()
+ {
+ var template = new ControlTemplate(typeof(Button));
+ var border = new FrameworkElementFactory(typeof(Border));
+ border.SetValue(Border.BackgroundProperty, new TemplateBindingExtension(Button.BackgroundProperty));
+ border.SetValue(Border.BorderBrushProperty, new TemplateBindingExtension(Button.BorderBrushProperty));
+ border.SetValue(Border.BorderThicknessProperty, new TemplateBindingExtension(Button.BorderThicknessProperty));
+ border.SetValue(Border.CornerRadiusProperty, new CornerRadius(8));
+ border.SetValue(Border.PaddingProperty, new TemplateBindingExtension(Button.PaddingProperty));
+ var presenter = new FrameworkElementFactory(typeof(ContentPresenter));
+ presenter.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Center);
+ presenter.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
+ border.AppendChild(presenter);
+ template.VisualTree = border;
+ return template;
+ }
+}
diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs
index 9bafc11..66ae540 100644
--- a/src/AxCopilot/Views/ChatWindow.xaml.cs
+++ b/src/AxCopilot/Views/ChatWindow.xaml.cs
@@ -1608,152 +1608,6 @@ public partial class ChatWindow : Window
SkillService.ActivateConditionalSkillsForPaths(_attachedFiles, cwd);
}
- private Popup? _sharedContextPopup;
-
- private (Popup Popup, StackPanel Panel) CreateThemedPopupMenu(
- UIElement? placementTarget = null,
- PlacementMode placement = PlacementMode.MousePoint,
- double minWidth = 200)
- {
- _sharedContextPopup?.SetCurrentValue(Popup.IsOpenProperty, false);
-
- var bg = TryFindResource("LauncherBackground") as Brush ?? Brushes.White;
- var border = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
- var panel = new StackPanel { Margin = new Thickness(2) };
- var container = new Border
- {
- Background = bg,
- BorderBrush = border,
- BorderThickness = new Thickness(1),
- CornerRadius = new CornerRadius(10),
- Padding = new Thickness(6),
- MinWidth = minWidth,
- Child = panel,
- Effect = new System.Windows.Media.Effects.DropShadowEffect
- {
- BlurRadius = 16,
- ShadowDepth = 3,
- Opacity = 0.18,
- Color = Colors.Black,
- Direction = 270,
- },
- };
-
- var popup = new Popup
- {
- Child = container,
- StaysOpen = false,
- AllowsTransparency = true,
- PopupAnimation = PopupAnimation.Fade,
- Placement = placement,
- PlacementTarget = placementTarget,
- };
-
- _sharedContextPopup = popup;
- return (popup, panel);
- }
-
- private Border CreatePopupMenuItem(
- Popup popup,
- string icon,
- string label,
- Brush iconBrush,
- Brush labelBrush,
- Brush hoverBrush,
- Action action)
- {
- var sp = new StackPanel { Orientation = Orientation.Horizontal };
- sp.Children.Add(new TextBlock
- {
- Text = icon,
- FontFamily = new FontFamily("Segoe MDL2 Assets"),
- FontSize = 12.5,
- Foreground = iconBrush,
- VerticalAlignment = VerticalAlignment.Center,
- Margin = new Thickness(0, 0, 9, 0),
- });
- sp.Children.Add(new TextBlock
- {
- Text = label,
- FontSize = 12.5,
- Foreground = labelBrush,
- VerticalAlignment = VerticalAlignment.Center,
- });
-
- var item = new Border
- {
- Child = sp,
- Background = Brushes.Transparent,
- CornerRadius = new CornerRadius(8),
- Cursor = Cursors.Hand,
- Padding = new Thickness(10, 7, 12, 7),
- Margin = new Thickness(0, 1, 0, 1),
- };
- item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBrush; };
- item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
- item.MouseLeftButtonUp += (_, _) =>
- {
- popup.SetCurrentValue(Popup.IsOpenProperty, false);
- action();
- };
-
- return item;
- }
-
- private static void AddPopupMenuSeparator(Panel panel, Brush brush)
- {
- panel.Children.Add(new Border
- {
- Height = 1,
- Margin = new Thickness(10, 4, 10, 4),
- Background = brush,
- Opacity = 0.35,
- });
- }
-
- /// 최근 폴더 항목 우클릭 컨텍스트 메뉴를 표시합니다.
- private void ShowRecentFolderContextMenu(string folderPath)
- {
- 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 hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
- var warningBrush = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44));
- var (popup, panel) = CreateThemedPopupMenu();
-
- panel.Children.Add(CreatePopupMenuItem(popup, "\uED25", "폴더 열기", secondaryText, primaryText, hoverBg, () =>
- {
- try
- {
- System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
- {
- FileName = folderPath,
- UseShellExecute = true,
- });
- }
- catch { }
- }));
-
- panel.Children.Add(CreatePopupMenuItem(popup, "\uE8C8", "경로 복사", secondaryText, primaryText, hoverBg, () =>
- {
- try { Clipboard.SetText(folderPath); } catch { }
- }));
-
- AddPopupMenuSeparator(panel, borderBrush);
-
- panel.Children.Add(CreatePopupMenuItem(popup, "\uE74D", "목록에서 삭제", warningBrush, warningBrush, hoverBg, () =>
- {
- _settings.Settings.Llm.RecentWorkFolders.RemoveAll(
- p => p.Equals(folderPath, StringComparison.OrdinalIgnoreCase));
- _settings.Save();
- // 메뉴 새로고침
- if (FolderMenuPopup.IsOpen)
- ShowFolderMenu();
- }));
-
- Dispatcher.BeginInvoke(() => { popup.IsOpen = true; }, DispatcherPriority.Input);
- }
-
private void BtnFolderClear_Click(object sender, RoutedEventArgs e)
{
FolderPathLabel.Text = "폴더를 선택하세요";
@@ -4014,242 +3868,6 @@ public partial class ChatWindow : Window
}
}
- /// 마우스 오버 시 살짝 확대 + 복귀하는 호버 애니메이션을 적용합니다.
- ///
- /// 마우스 오버 시 살짝 확대하는 호버 애니메이션.
- /// 주의: 인접 요소(탭 버튼, 가로 나열 메뉴 등)에는 사용 금지 — 확대 시 이웃 요소를 가립니다.
- /// 독립적 공간이 있는 버튼에만 적용하세요.
- ///
- private static void ApplyHoverScaleAnimation(FrameworkElement element, double hoverScale = 1.08)
- {
- // Loaded 이벤트에서 실행해야 XAML Style의 봉인된 Transform을 안전하게 교체 가능
- void EnsureTransform()
- {
- element.RenderTransformOrigin = new Point(0.5, 0.5);
- // 봉인(frozen)된 Transform이면 새로 생성하여 교체
- if (element.RenderTransform is not ScaleTransform || element.RenderTransform.IsFrozen)
- element.RenderTransform = new ScaleTransform(1, 1);
- }
-
- element.Loaded += (_, _) => EnsureTransform();
-
- element.MouseEnter += (_, _) =>
- {
- EnsureTransform();
- var st = (ScaleTransform)element.RenderTransform;
- var grow = new DoubleAnimation(hoverScale, TimeSpan.FromMilliseconds(150))
- { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } };
- st.BeginAnimation(ScaleTransform.ScaleXProperty, grow);
- st.BeginAnimation(ScaleTransform.ScaleYProperty, grow);
- };
- element.MouseLeave += (_, _) =>
- {
- EnsureTransform();
- var st = (ScaleTransform)element.RenderTransform;
- var shrink = new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(200))
- { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } };
- st.BeginAnimation(ScaleTransform.ScaleXProperty, shrink);
- st.BeginAnimation(ScaleTransform.ScaleYProperty, shrink);
- };
- }
-
- /// 마우스 오버 시 텍스트가 살짝 튀어오르는 바운스 애니메이션을 적용합니다.
- ///
- /// 마우스 오버 시 텍스트가 살짝 튀어오르는 바운스 애니메이션.
- /// Scale과 달리 크기가 변하지 않아 인접 요소를 가리지 않습니다.
- ///
- private static void ApplyHoverBounceAnimation(FrameworkElement element, double bounceY = -2.5)
- {
- void EnsureTransform()
- {
- if (element.RenderTransform is not TranslateTransform || element.RenderTransform.IsFrozen)
- element.RenderTransform = new TranslateTransform(0, 0);
- }
-
- element.Loaded += (_, _) => EnsureTransform();
-
- element.MouseEnter += (_, _) =>
- {
- EnsureTransform();
- var tt = (TranslateTransform)element.RenderTransform;
- tt.BeginAnimation(TranslateTransform.YProperty,
- new DoubleAnimation(bounceY, TimeSpan.FromMilliseconds(200))
- { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } });
- };
- element.MouseLeave += (_, _) =>
- {
- EnsureTransform();
- var tt = (TranslateTransform)element.RenderTransform;
- tt.BeginAnimation(TranslateTransform.YProperty,
- new DoubleAnimation(0, TimeSpan.FromMilliseconds(250))
- { EasingFunction = new ElasticEase { EasingMode = EasingMode.EaseOut, Oscillations = 1, Springiness = 10 } });
- };
- }
-
- /// 심플한 V 체크 아이콘을 생성합니다 (디자인 통일용).
- private static FrameworkElement CreateSimpleCheck(Brush color, double size = 14)
- {
- return new System.Windows.Shapes.Path
- {
- Data = Geometry.Parse($"M {size * 0.15} {size * 0.5} L {size * 0.4} {size * 0.75} L {size * 0.85} {size * 0.28}"),
- Stroke = color,
- StrokeThickness = 2,
- StrokeStartLineCap = PenLineCap.Round,
- StrokeEndLineCap = PenLineCap.Round,
- StrokeLineJoin = PenLineJoin.Round,
- Width = size,
- Height = size,
- Margin = new Thickness(0, 0, 10, 0),
- VerticalAlignment = VerticalAlignment.Center,
- };
- }
-
- /// 팝업 메뉴 항목에 호버 배경색 + 미세 확대 효과를 적용합니다.
- private static void ApplyMenuItemHover(Border item)
- {
- var originalBg = item.Background?.Clone() ?? Brushes.Transparent;
- if (originalBg.CanFreeze) originalBg.Freeze();
- item.RenderTransformOrigin = new Point(0.5, 0.5);
- item.RenderTransform = new ScaleTransform(1, 1);
- item.MouseEnter += (s, _) =>
- {
- if (s is Border b)
- {
- // 원래 배경이 투명이면 반투명 흰색, 아니면 밝기 변경
- if (originalBg is SolidColorBrush scb && scb.Color.A > 0x20)
- b.Opacity = 0.85;
- else
- b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
- }
- var st = item.RenderTransform as ScaleTransform;
- st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
- st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
- };
- item.MouseLeave += (s, _) =>
- {
- if (s is Border b)
- {
- b.Opacity = 1.0;
- b.Background = originalBg;
- }
- var st = item.RenderTransform as ScaleTransform;
- st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
- st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
- };
- }
-
- private Button CreateActionButton(string symbol, string tooltip, Brush foreground, Action onClick)
- {
- var hoverBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
- var hoverBg = TryFindResource("ItemHoverBackground") as Brush
- ?? new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF));
- var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
- var icon = new TextBlock
- {
- Text = symbol,
- FontFamily = new FontFamily("Segoe MDL2 Assets"),
- FontSize = 10,
- Foreground = foreground,
- VerticalAlignment = VerticalAlignment.Center
- };
- var btn = new Button
- {
- Content = icon,
- Background = Brushes.Transparent,
- BorderBrush = borderBrush,
- BorderThickness = new Thickness(1),
- Cursor = Cursors.Hand,
- Width = 24,
- Height = 24,
- Padding = new Thickness(0),
- Margin = new Thickness(0, 0, 2, 0),
- ToolTip = tooltip
- };
- btn.Template = BuildMinimalIconButtonTemplate();
- btn.MouseEnter += (_, _) =>
- {
- icon.Foreground = hoverBrush;
- btn.Background = hoverBg;
- };
- btn.MouseLeave += (_, _) =>
- {
- icon.Foreground = foreground;
- btn.Background = Brushes.Transparent;
- };
- btn.Click += (_, _) => onClick();
- return btn;
- }
-
- private void ShowMessageActionBar(StackPanel actionBar)
- {
- if (actionBar == null)
- return;
-
- actionBar.Opacity = 1;
- }
-
- private void HideMessageActionBarIfNotSelected(StackPanel actionBar)
- {
- if (actionBar == null)
- return;
-
- if (!ReferenceEquals(_selectedMessageActionBar, actionBar))
- actionBar.Opacity = 0;
- }
-
- private void SelectMessageActionBar(StackPanel actionBar, Border? messageBorder = null)
- {
- if (_selectedMessageActionBar != null && !ReferenceEquals(_selectedMessageActionBar, actionBar))
- _selectedMessageActionBar.Opacity = 0;
-
- if (_selectedMessageBorder != null && !ReferenceEquals(_selectedMessageBorder, messageBorder))
- ApplyMessageSelectionStyle(_selectedMessageBorder, false);
-
- _selectedMessageActionBar = actionBar;
- _selectedMessageActionBar.Opacity = 1;
- _selectedMessageBorder = messageBorder;
- if (_selectedMessageBorder != null)
- ApplyMessageSelectionStyle(_selectedMessageBorder, true);
- }
-
- private void ApplyMessageSelectionStyle(Border border, bool selected)
- {
- if (border == null)
- return;
-
- var accent = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
- var defaultBorder = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
- border.BorderBrush = selected ? accent : defaultBorder;
- border.BorderThickness = selected ? new Thickness(1.5) : new Thickness(1);
- border.Effect = selected
- ? new System.Windows.Media.Effects.DropShadowEffect
- {
- BlurRadius = 16,
- ShadowDepth = 0,
- Opacity = 0.10,
- Color = Colors.Black,
- }
- : null;
- }
-
- private static ControlTemplate BuildMinimalIconButtonTemplate()
- {
- var template = new ControlTemplate(typeof(Button));
- var border = new FrameworkElementFactory(typeof(Border));
- border.SetValue(Border.BackgroundProperty, new TemplateBindingExtension(Button.BackgroundProperty));
- border.SetValue(Border.BorderBrushProperty, new TemplateBindingExtension(Button.BorderBrushProperty));
- border.SetValue(Border.BorderThicknessProperty, new TemplateBindingExtension(Button.BorderThicknessProperty));
- border.SetValue(Border.CornerRadiusProperty, new CornerRadius(8));
- border.SetValue(Border.PaddingProperty, new TemplateBindingExtension(Button.PaddingProperty));
- var presenter = new FrameworkElementFactory(typeof(ContentPresenter));
- presenter.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Center);
- presenter.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
- border.AppendChild(presenter);
- template.VisualTree = border;
- return template;
- }
-
-
// ─── 스트리밍 커서 깜빡임 + AI 아이콘 펄스 ────────────────────────────
private void StopAiIconPulse()