using System.Runtime.InteropServices; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Media.Effects; namespace AxCopilot.Views; /// /// 에이전트 실행 계획을 상세히 보여주는 별도 창. /// - 항목 기본 접힘 / 클릭으로 펼침 /// - 모두 열기 / 모두 닫기 툴바 /// - 사방 가장자리 드래그 리사이즈 /// - 항목 드래그로 순서 변경 /// internal sealed class PlanViewerWindow : Window { // ── Win32 Resize ── [DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); private const int WM_NCHITTEST = 0x0084; private const int HTLEFT = 10, HTRIGHT = 11, HTTOP = 12, HTTOPLEFT = 13, HTTOPRIGHT = 14, HTBOTTOM = 15, HTBOTTOMLEFT = 16, HTBOTTOMRIGHT = 17; private const int ResizeGrip = 12; // 사방 12px 영역에서 리사이즈 가능 private const string DragDataFormat = "PlanStepIndex"; private readonly StackPanel _stepsPanel; private readonly ScrollViewer _scrollViewer; private readonly StackPanel _btnPanel; private readonly Border _statusBar; private readonly TextBlock _statusText; private readonly TextBlock _progressText; // 펼침 상태 관리 private readonly HashSet _expandedSteps = new(); // 드래그 상태 private int _dragSourceIndex = -1; private Point _dragStartPoint; private TaskCompletionSource? _tcs; private string _planText = ""; private List _steps = new(); private int _currentStep = -1; private bool _isExecuting; private readonly string _uiExpressionLevel; public PlanViewerWindow(Window? owner = null) { if (owner != null) { Owner = owner; WindowStartupLocation = WindowStartupLocation.CenterOwner; Resources.MergedDictionaries.Add(owner.Resources); } _uiExpressionLevel = ResolveUiExpressionLevel(); Width = 640; Height = 520; MinWidth = 480; MinHeight = 360; WindowStartupLocation = owner == null ? WindowStartupLocation.CenterScreen : WindowStartupLocation.CenterOwner; WindowStyle = WindowStyle.None; AllowsTransparency = true; Background = Brushes.Transparent; ResizeMode = ResizeMode.CanResize; // WndProc로 직접 처리 ShowInTaskbar = false; var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E)); var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var accentBrush = TryFindResource("AccentColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var root = new Border { Background = bgBrush, CornerRadius = new CornerRadius(14), BorderBrush = borderBrush, BorderThickness = new Thickness(1), Effect = new DropShadowEffect { BlurRadius = 20, ShadowDepth = 4, Opacity = 0.35, Color = Colors.Black }, }; var mainGrid = new Grid(); mainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // 0: 타이틀 바 mainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // 1: 상태 바 mainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // 2: 툴바 (모두 열기/닫기) mainGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); // 3: 단계 목록 mainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // 4: 하단 버튼 // ── 타이틀 바 ── var titleBar = new Grid { Background = Brushes.Transparent, Margin = new Thickness(20, 14, 12, 0) }; titleBar.MouseLeftButtonDown += TitleBar_MouseLeftButtonDown; var titleSp = new StackPanel { Orientation = Orientation.Horizontal }; titleSp.Children.Add(new TextBlock { Text = "\uE9D2", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 18, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0), }); titleSp.Children.Add(new TextBlock { Text = "실행 계획", FontSize = 16, FontWeight = FontWeights.Bold, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, }); titleBar.Children.Add(titleSp); var closeBtn = new Border { Width = 32, Height = 32, CornerRadius = new CornerRadius(8), Background = Brushes.Transparent, Cursor = Cursors.Hand, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center, Child = new TextBlock { Text = "\uE8BB", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = secondaryText, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }, }; closeBtn.MouseEnter += (s, _) => ((Border)s).Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); closeBtn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent; closeBtn.MouseLeftButtonUp += (_, _) => Hide(); titleBar.Children.Add(closeBtn); Grid.SetRow(titleBar, 0); mainGrid.Children.Add(titleBar); // ── 상태 바 (진행률) ── _statusBar = new Border { Visibility = Visibility.Collapsed, Margin = new Thickness(20, 8, 20, 0), Padding = new Thickness(12, 6, 12, 6), CornerRadius = new CornerRadius(8), Background = new SolidColorBrush(Color.FromArgb(0x15, ((SolidColorBrush)accentBrush).Color.R, ((SolidColorBrush)accentBrush).Color.G, ((SolidColorBrush)accentBrush).Color.B)), }; var statusGrid = new Grid(); _statusText = new TextBlock { Text = "실행 중...", FontSize = 12, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center }; statusGrid.Children.Add(_statusText); _progressText = new TextBlock { Text = "", FontSize = 12, Foreground = secondaryText, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center, }; statusGrid.Children.Add(_progressText); _statusBar.Child = statusGrid; Grid.SetRow(_statusBar, 1); mainGrid.Children.Add(_statusBar); // ── 툴바: 모두 열기 / 모두 닫기 ── var hoverBgTb = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); var toolBar = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(20, 6, 20, 0), }; if (_uiExpressionLevel == "simple") toolBar.Visibility = Visibility.Collapsed; var expandAllBtn = MakeToolbarButton("\uE70D", "모두 열기", secondaryText, hoverBgTb); expandAllBtn.MouseLeftButtonUp += (_, _) => { for (int i = 0; i < _steps.Count; i++) _expandedSteps.Add(i); RenderSteps(); }; toolBar.Children.Add(expandAllBtn); var collapseAllBtn = MakeToolbarButton("\uE70E", "모두 닫기", secondaryText, hoverBgTb); collapseAllBtn.MouseLeftButtonUp += (_, _) => { _expandedSteps.Clear(); RenderSteps(); }; toolBar.Children.Add(collapseAllBtn); Grid.SetRow(toolBar, 2); mainGrid.Children.Add(toolBar); // ── 컨텐츠: 단계 목록 ── _scrollViewer = new ScrollViewer { VerticalScrollBarVisibility = ScrollBarVisibility.Auto, Margin = new Thickness(16, 6, 16, 0), Padding = new Thickness(4), }; _stepsPanel = new StackPanel(); _scrollViewer.Content = _stepsPanel; Grid.SetRow(_scrollViewer, 3); mainGrid.Children.Add(_scrollViewer); // ── 하단 버튼 패널 ── _btnPanel = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(20, 12, 20, 16), }; Grid.SetRow(_btnPanel, 4); mainGrid.Children.Add(_btnPanel); root.Child = mainGrid; Content = root; // Win32 Resize 훅 SourceInitialized += (_, _) => { var hwnd = new WindowInteropHelper(this).Handle; var src = HwndSource.FromHwnd(hwnd); src?.AddHook(WndProc); }; } // ════════════════════════════════════════════════════════════ // 툴바 버튼 팩토리 // ════════════════════════════════════════════════════════════ private static Border MakeToolbarButton(string icon, string label, Brush fg, Brush hoverBg) { var btn = new Border { CornerRadius = new CornerRadius(6), Padding = new Thickness(8, 3, 8, 3), Margin = new Thickness(0, 0, 4, 0), Background = Brushes.Transparent, Cursor = Cursors.Hand, }; var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 9, Foreground = fg, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0), }); sp.Children.Add(new TextBlock { Text = label, FontSize = 11.5, Foreground = fg }); btn.Child = sp; btn.MouseEnter += (s, _) => ((Border)s).Background = hoverBg; btn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent; return btn; } // ════════════════════════════════════════════════════════════ // 창 이동 / 리사이즈 // ════════════════════════════════════════════════════════════ private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (e.ClickCount == 2) return; try { DragMove(); } catch { } } private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == WM_NCHITTEST) { var pt = PointFromScreen(new Point( (short)(lParam.ToInt32() & 0xFFFF), (short)(lParam.ToInt32() >> 16))); var w = ActualWidth; var h = ActualHeight; int hit = 0; if (pt.X < ResizeGrip && pt.Y < ResizeGrip) hit = HTTOPLEFT; else if (pt.X > w - ResizeGrip && pt.Y < ResizeGrip) hit = HTTOPRIGHT; else if (pt.X < ResizeGrip && pt.Y > h - ResizeGrip) hit = HTBOTTOMLEFT; else if (pt.X > w - ResizeGrip && pt.Y > h - ResizeGrip) hit = HTBOTTOMRIGHT; else if (pt.X < ResizeGrip) hit = HTLEFT; else if (pt.X > w - ResizeGrip) hit = HTRIGHT; else if (pt.Y < ResizeGrip) hit = HTTOP; else if (pt.Y > h - ResizeGrip) hit = HTBOTTOM; if (hit != 0) { handled = true; return (IntPtr)hit; } } return IntPtr.Zero; } // ════════════════════════════════════════════════════════════ // 공개 API // ════════════════════════════════════════════════════════════ public void LoadPlan(string planText, List steps, TaskCompletionSource tcs) { _planText = planText; _steps = steps; _tcs = tcs; _currentStep = -1; _isExecuting = false; _expandedSteps.Clear(); // 새 계획 표시 시 모두 접힌 상태로 시작 if (_uiExpressionLevel == "rich") { for (int i = 0; i < _steps.Count; i++) _expandedSteps.Add(i); } RenderSteps(); BuildApprovalButtons(); _statusBar.Visibility = Visibility.Collapsed; } public void LoadPlanPreview(string planText, List steps) { _planText = planText; _steps = steps; _tcs = null; _currentStep = -1; _isExecuting = false; _expandedSteps.Clear(); if (_uiExpressionLevel == "rich") { for (int i = 0; i < _steps.Count; i++) _expandedSteps.Add(i); } RenderSteps(); BuildCloseButton(); _statusBar.Visibility = Visibility.Collapsed; } public Task ShowPlanAsync(string planText, List steps, TaskCompletionSource tcs) { LoadPlan(planText, steps, tcs); Show(); Activate(); return tcs.Task; } public void SwitchToExecutionMode() { _isExecuting = true; _statusBar.Visibility = Visibility.Visible; _statusText.Text = "▶ 계획 실행 중..."; _progressText.Text = $"0 / {_steps.Count}"; BuildExecutionButtons(); } public void UpdateCurrentStep(int stepIndex) { if (stepIndex < 0 || stepIndex >= _steps.Count) return; _currentStep = stepIndex; _progressText.Text = $"{stepIndex + 1} / {_steps.Count}"; _expandedSteps.Add(stepIndex); // 현재 실행 중인 단계는 자동 펼침 RenderSteps(); } public void MarkComplete() { _isExecuting = false; _statusText.Text = "✓ 계획 실행 완료"; _statusText.Foreground = new SolidColorBrush(Color.FromRgb(0x10, 0xB9, 0x81)); _progressText.Text = $"{_steps.Count} / {_steps.Count}"; _currentStep = _steps.Count; RenderSteps(); BuildCloseButton(); } public string PlanText => _planText; public List Steps => _steps; public string? BuildApprovedDecisionPayload(string prefix) { if (string.IsNullOrWhiteSpace(prefix)) return null; var normalized = _steps .Select(step => step?.Trim() ?? "") .Where(step => !string.IsNullOrWhiteSpace(step)) .ToList(); if (normalized.Count == 0) return null; var sb = new StringBuilder(); sb.AppendLine(prefix.Trim()); foreach (var step in normalized) sb.AppendLine(step); return sb.ToString().TrimEnd(); } // ════════════════════════════════════════════════════════════ // 단계 목록 렌더링 // ════════════════════════════════════════════════════════════ private void RenderSteps() { _stepsPanel.Children.Clear(); var primaryText = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); var itemBg = Application.Current.TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40)); var hoverBg = Application.Current.TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); var canEdit = !_isExecuting && _currentStep < 0; // 승인 대기 중에만 편집/순서변경 가능 var summaryText = _uiExpressionLevel switch { "simple" => _isExecuting ? "진행률을 간단히 표시합니다." : $"{_steps.Count}단계 계획입니다. 핵심 단계만 확인하고 승인하세요.", "rich" => _isExecuting ? "현재 단계를 기준으로 진행률을 표시합니다. 필요 시 단계를 펼쳐 세부 내용을 확인할 수 있습니다." : $"총 {_steps.Count}단계입니다. 단계별 내용을 열어 우선순위/의존성을 검토한 뒤 승인 또는 수정 요청을 선택하세요.", _ => _isExecuting ? "현재 단계를 기준으로 진행률을 표시합니다." : $"총 {_steps.Count}단계입니다. 단계를 펼쳐 검토한 후 승인 또는 수정 요청을 선택하세요.", }; _stepsPanel.Children.Add(new Border { Background = new SolidColorBrush(Color.FromArgb(0x12, ((SolidColorBrush)accentBrush).Color.R, ((SolidColorBrush)accentBrush).Color.G, ((SolidColorBrush)accentBrush).Color.B)), BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, ((SolidColorBrush)accentBrush).Color.R, ((SolidColorBrush)accentBrush).Color.G, ((SolidColorBrush)accentBrush).Color.B)), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(10), Padding = new Thickness(10, 8, 10, 8), Margin = new Thickness(0, 0, 0, 8), Child = new StackPanel { Children = { new TextBlock { Text = _isExecuting ? "계획 실행 중" : "계획 승인 대기", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = accentBrush, }, new TextBlock { Text = summaryText, FontSize = 11.5, Foreground = secondaryText, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 3, 0, 0), } } } }); for (int i = 0; i < _steps.Count; i++) { var step = _steps[i]; var capturedIdx = i; var isComplete = i < _currentStep; var isCurrent = i == _currentStep; var isPending = i > _currentStep; var isExpanded = _expandedSteps.Contains(i); // ─ 카드 Border ─ var card = new Border { CornerRadius = new CornerRadius(10), Padding = new Thickness(10, 7, 10, 7), Margin = new Thickness(0, 0, 0, 5), Background = isCurrent ? new SolidColorBrush(Color.FromArgb(0x18, ((SolidColorBrush)accentBrush).Color.R, ((SolidColorBrush)accentBrush).Color.G, ((SolidColorBrush)accentBrush).Color.B)) : itemBg, BorderBrush = isCurrent ? accentBrush : Brushes.Transparent, BorderThickness = new Thickness(isCurrent ? 1.5 : 0), AllowDrop = canEdit, }; // 열기/닫기 토글 — 텍스트 또는 배경 클릭 card.Cursor = Cursors.Hand; card.MouseLeftButtonUp += (s, e) => { // 드래그 직후 클릭이 발생하는 경우 무시 if (e.OriginalSource is Border src && src.Tag?.ToString() == "DragHandle") return; if (_expandedSteps.Contains(capturedIdx)) _expandedSteps.Remove(capturedIdx); else _expandedSteps.Add(capturedIdx); RenderSteps(); }; // ─ 카드 Grid: [drag?][badge][*text][chevron][edit?] ─ var cardGrid = new Grid(); int badgeCol = canEdit ? 1 : 0; int textCol = canEdit ? 2 : 1; int chevCol = canEdit ? 3 : 2; int editCol = canEdit ? 4 : -1; if (canEdit) cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // drag cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // badge cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); // text cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // chevron if (canEdit) cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // edit btns // ── 드래그 핸들 (편집 모드 전용) ── if (canEdit) { var dimColor = Color.FromArgb(0x55, 0x80, 0x80, 0x80); var dimBrush = new SolidColorBrush(dimColor); var dragHandle = new Border { Tag = "DragHandle", Width = 20, Cursor = Cursors.SizeAll, Background = Brushes.Transparent, Margin = new Thickness(0, 0, 6, 0), VerticalAlignment = VerticalAlignment.Center, Child = new TextBlock { Text = "\uE8FD", // Sort/Lines 아이콘 (드래그 핸들) FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11, Foreground = dimBrush, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }, }; dragHandle.MouseEnter += (s, _) => ((TextBlock)((Border)s).Child).Foreground = secondaryText; dragHandle.MouseLeave += (s, _) => ((TextBlock)((Border)s).Child).Foreground = dimBrush; // 드래그 시작 — 마우스 눌림 위치 기록 dragHandle.PreviewMouseLeftButtonDown += (s, e) => { _dragSourceIndex = capturedIdx; _dragStartPoint = e.GetPosition(_stepsPanel); e.Handled = true; // 카드 클릭(expand) 이벤트 방지 }; // 충분히 움직이면 DragDrop 시작 dragHandle.PreviewMouseMove += (s, e) => { if (_dragSourceIndex < 0 || e.LeftButton != MouseButtonState.Pressed) return; var cur = e.GetPosition(_stepsPanel); if (Math.Abs(cur.X - _dragStartPoint.X) > SystemParameters.MinimumHorizontalDragDistance || Math.Abs(cur.Y - _dragStartPoint.Y) > SystemParameters.MinimumVerticalDragDistance) { int idx = _dragSourceIndex; _dragSourceIndex = -1; DragDrop.DoDragDrop((DependencyObject)s, new DataObject(DragDataFormat, idx), DragDropEffects.Move); // DoDragDrop 완료 후 비주얼 정리 Dispatcher.InvokeAsync(RenderSteps); } }; dragHandle.PreviewMouseLeftButtonUp += (_, _) => _dragSourceIndex = -1; Grid.SetColumn(dragHandle, 0); cardGrid.Children.Add(dragHandle); // ── 카드 Drop 이벤트 ── card.DragOver += (s, e) => { if (!e.Data.GetDataPresent(DragDataFormat)) return; int src = (int)e.Data.GetData(DragDataFormat); if (src != capturedIdx) { ((Border)s).BorderBrush = accentBrush; ((Border)s).BorderThickness = new Thickness(1.5); e.Effects = DragDropEffects.Move; } else e.Effects = DragDropEffects.None; e.Handled = true; }; card.DragLeave += (s, _) => { bool isCurr = _currentStep == capturedIdx; ((Border)s).BorderBrush = isCurr ? accentBrush : Brushes.Transparent; ((Border)s).BorderThickness = new Thickness(isCurr ? 1.5 : 0); }; card.Drop += (s, e) => { if (!e.Data.GetDataPresent(DragDataFormat)) { e.Handled = true; return; } int srcIdx = (int)e.Data.GetData(DragDataFormat); int dstIdx = capturedIdx; if (srcIdx != dstIdx && srcIdx >= 0 && srcIdx < _steps.Count) { var item = _steps[srcIdx]; _steps.RemoveAt(srcIdx); // srcIdx < dstIdx 이면 제거 후 인덱스가 1 감소 int insertAt = srcIdx < dstIdx ? dstIdx - 1 : dstIdx; _steps.Insert(insertAt, item); _expandedSteps.Clear(); RenderSteps(); } e.Handled = true; }; } // ── 상태 배지 ── UIElement badge; if (isComplete) { badge = new TextBlock { Text = "\uE73E", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 13, Foreground = new SolidColorBrush(Color.FromRgb(0x10, 0xB9, 0x81)), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0), Width = 20, TextAlignment = TextAlignment.Center, }; } else if (isCurrent) { badge = new TextBlock { Text = "\uE768", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 13, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0), Width = 20, TextAlignment = TextAlignment.Center, }; } else { badge = new Border { Width = 22, Height = 22, CornerRadius = new CornerRadius(11), Background = new SolidColorBrush(Color.FromArgb(0x25, 0x80, 0x80, 0x80)), Margin = new Thickness(0, 0, 10, 0), VerticalAlignment = VerticalAlignment.Center, Child = new TextBlock { Text = $"{i + 1}", FontSize = 11, Foreground = secondaryText, FontWeight = FontWeights.SemiBold, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }, }; } Grid.SetColumn(badge, badgeCol); cardGrid.Children.Add(badge); // ── 단계 텍스트 ── var textBlock = new TextBlock { Text = step, FontSize = 13, Foreground = isComplete ? secondaryText : primaryText, VerticalAlignment = VerticalAlignment.Center, Opacity = isPending && _isExecuting ? 0.6 : 1.0, TextDecorations = isComplete ? TextDecorations.Strikethrough : null, Margin = new Thickness(0, 0, 4, 0), }; if (isExpanded) { textBlock.TextWrapping = TextWrapping.Wrap; textBlock.TextTrimming = TextTrimming.None; } else { textBlock.TextWrapping = TextWrapping.NoWrap; textBlock.TextTrimming = TextTrimming.CharacterEllipsis; textBlock.ToolTip = step; // 접힌 상태: 호버 시 전체 텍스트 툴팁 } Grid.SetColumn(textBlock, textCol); cardGrid.Children.Add(textBlock); // ── 펼침/접힘 Chevron ── var chevron = new Border { Width = 22, Height = 22, CornerRadius = new CornerRadius(4), Background = Brushes.Transparent, Cursor = Cursors.Hand, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, canEdit ? 4 : 0, 0), Child = new TextBlock { Text = isExpanded ? "\uE70E" : "\uE70D", // ChevronUp / ChevronDown FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 9, Foreground = new SolidColorBrush(Color.FromArgb(0x70, 0x80, 0x80, 0x80)), HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }, }; chevron.MouseEnter += (s, _) => ((Border)s).Background = hoverBg; chevron.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent; chevron.MouseLeftButtonUp += (_, e) => { if (_expandedSteps.Contains(capturedIdx)) _expandedSteps.Remove(capturedIdx); else _expandedSteps.Add(capturedIdx); RenderSteps(); e.Handled = true; }; Grid.SetColumn(chevron, chevCol); cardGrid.Children.Add(chevron); // ── 편집 버튼 (위/아래/편집/삭제) ── if (canEdit) { var editBtnPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(2, 0, 0, 0), }; if (i > 0) { var upBtn = CreateMiniButton("\uE70E", secondaryText, hoverBg); upBtn.ToolTip = "위로 이동"; upBtn.MouseLeftButtonUp += (_, e) => { SwapSteps(capturedIdx, capturedIdx - 1); e.Handled = true; }; editBtnPanel.Children.Add(upBtn); } if (i < _steps.Count - 1) { var downBtn = CreateMiniButton("\uE70D", secondaryText, hoverBg); downBtn.ToolTip = "아래로 이동"; downBtn.MouseLeftButtonUp += (_, e) => { SwapSteps(capturedIdx, capturedIdx + 1); e.Handled = true; }; editBtnPanel.Children.Add(downBtn); } var editBtn = CreateMiniButton("\uE70F", accentBrush, hoverBg); editBtn.ToolTip = "편집"; editBtn.MouseLeftButtonUp += (_, e) => { EditStep(capturedIdx); e.Handled = true; }; editBtnPanel.Children.Add(editBtn); var delBtn = CreateMiniButton("\uE74D", new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)), hoverBg); delBtn.ToolTip = "삭제"; delBtn.MouseLeftButtonUp += (_, e) => { if (_steps.Count > 1) { _steps.RemoveAt(capturedIdx); _expandedSteps.Remove(capturedIdx); RenderSteps(); } e.Handled = true; }; editBtnPanel.Children.Add(delBtn); Grid.SetColumn(editBtnPanel, editCol); cardGrid.Children.Add(editBtnPanel); } card.Child = cardGrid; _stepsPanel.Children.Add(card); } // ── 단계 추가 버튼 (편집 모드) ── if (canEdit) { var st2 = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var hb2 = Application.Current.TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); var addBtn = new Border { CornerRadius = new CornerRadius(10), Padding = new Thickness(14, 8, 14, 8), Margin = new Thickness(0, 4, 0, 0), Background = Brushes.Transparent, BorderBrush = new SolidColorBrush(Color.FromArgb(0x30, 0x80, 0x80, 0x80)), BorderThickness = new Thickness(1), Cursor = Cursors.Hand, }; var addSp = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center }; addSp.Children.Add(new TextBlock { Text = "\uE710", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = st2, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), }); addSp.Children.Add(new TextBlock { Text = "단계 추가", FontSize = 12, Foreground = st2 }); addBtn.Child = addSp; addBtn.MouseEnter += (s, _) => ((Border)s).Background = hb2; addBtn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent; addBtn.MouseLeftButtonUp += (_, _) => { _steps.Add("새 단계"); RenderSteps(); EditStep(_steps.Count - 1); }; _stepsPanel.Children.Add(addBtn); } // 현재 단계로 자동 스크롤 if (_currentStep >= 0 && _stepsPanel.Children.Count > _currentStep) { _stepsPanel.UpdateLayout(); var target = (FrameworkElement)_stepsPanel.Children[Math.Min(_currentStep, _stepsPanel.Children.Count - 1)]; target.BringIntoView(); } } // ════════════════════════════════════════════════════════════ // 단계 편집 / 교환 // ════════════════════════════════════════════════════════════ private void SwapSteps(int a, int b) { if (a < 0 || b < 0 || a >= _steps.Count || b >= _steps.Count) return; (_steps[a], _steps[b]) = (_steps[b], _steps[a]); RenderSteps(); } private void EditStep(int index) { if (index < 0 || index >= _steps.Count) return; var primaryText = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White; var itemBg = Application.Current.TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40)); var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); if (index >= _stepsPanel.Children.Count) return; var editCard = new Border { CornerRadius = new CornerRadius(10), Padding = new Thickness(10, 8, 10, 8), Margin = new Thickness(0, 0, 0, 5), Background = itemBg, BorderBrush = accentBrush, BorderThickness = new Thickness(1.5), }; var textBox = new TextBox { Text = _steps[index], FontSize = 13, Background = Brushes.Transparent, Foreground = primaryText, CaretBrush = primaryText, BorderThickness = new Thickness(0), AcceptsReturn = false, TextWrapping = TextWrapping.Wrap, Padding = new Thickness(4), }; var capturedIdx = index; textBox.KeyDown += (_, e) => { if (e.Key == Key.Enter) { _steps[capturedIdx] = textBox.Text.Trim(); RenderSteps(); e.Handled = true; } if (e.Key == Key.Escape) { RenderSteps(); e.Handled = true; } }; textBox.LostFocus += (_, _) => { _steps[capturedIdx] = textBox.Text.Trim(); RenderSteps(); }; editCard.Child = textBox; _stepsPanel.Children[index] = editCard; textBox.Focus(); textBox.SelectAll(); } // ════════════════════════════════════════════════════════════ // 하단 버튼 빌드 // ════════════════════════════════════════════════════════════ private void BuildApprovalButtons() { _btnPanel.Children.Clear(); var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); var approveLabel = _uiExpressionLevel == "simple" ? "승인" : "승인 후 실행"; var editLabel = _uiExpressionLevel == "simple" ? "수정" : "수정 피드백"; var rejectLabel = _uiExpressionLevel == "simple" ? "취소" : "거부"; var approveBtn = CreateActionButton("\uE73E", approveLabel, accentBrush, Brushes.White, true); approveBtn.MouseLeftButtonUp += (_, _) => { _tcs?.TrySetResult(null); SwitchToExecutionMode(); }; _btnPanel.Children.Add(approveBtn); var editBtn = CreateActionButton("\uE70F", editLabel, accentBrush, accentBrush, false); editBtn.MouseLeftButtonUp += (_, _) => ShowEditInput(); _btnPanel.Children.Add(editBtn); var cancelBrush = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)); var cancelBtn = CreateActionButton("\uE711", rejectLabel, cancelBrush, cancelBrush, false); cancelBtn.MouseLeftButtonUp += (_, _) => { _tcs?.TrySetResult("취소"); Hide(); }; _btnPanel.Children.Add(cancelBtn); } private void BuildExecutionButtons() { _btnPanel.Children.Clear(); var secondaryText = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var hideBtn = CreateActionButton("\uE921", "숨기기", secondaryText, Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White, false); hideBtn.MouseLeftButtonUp += (_, _) => Hide(); _btnPanel.Children.Add(hideBtn); } private void BuildCloseButton() { _btnPanel.Children.Clear(); var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); var closeBtn = CreateActionButton("\uE73E", "닫기", accentBrush, Brushes.White, true); closeBtn.MouseLeftButtonUp += (_, _) => Hide(); _btnPanel.Children.Add(closeBtn); } private void ShowEditInput() { var editPanel = new Border { Margin = new Thickness(20, 0, 20, 12), Padding = new Thickness(12, 8, 12, 8), CornerRadius = new CornerRadius(10), Background = Application.Current.TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40)), }; var editStack = new StackPanel(); editStack.Children.Add(new TextBlock { Text = "수정 사항을 입력하세요:", FontSize = 11.5, Foreground = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, Margin = new Thickness(0, 0, 0, 6), }); var textBox = new TextBox { MinHeight = 44, MaxHeight = 120, AcceptsReturn = true, TextWrapping = TextWrapping.Wrap, FontSize = 13, Background = Application.Current.TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E)), Foreground = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White, CaretBrush = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White, BorderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray, BorderThickness = new Thickness(1), Padding = new Thickness(10, 8, 10, 8), }; editStack.Children.Add(textBox); var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); var sendBtn = new Border { Background = accentBrush, CornerRadius = new CornerRadius(8), Padding = new Thickness(14, 6, 14, 6), Margin = new Thickness(0, 8, 0, 0), Cursor = Cursors.Hand, HorizontalAlignment = HorizontalAlignment.Right, Child = new TextBlock { Text = "전송", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White, }, }; sendBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85; sendBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0; sendBtn.MouseLeftButtonUp += (_, _) => { var feedback = textBox.Text.Trim(); if (string.IsNullOrEmpty(feedback)) return; _tcs?.TrySetResult(feedback); }; editStack.Children.Add(sendBtn); editPanel.Child = editStack; if (_btnPanel.Parent is Grid parentGrid) { for (int i = parentGrid.Children.Count - 1; i >= 0; i--) { if (parentGrid.Children[i] is Border b && b.Tag?.ToString() == "EditPanel") parentGrid.Children.RemoveAt(i); } editPanel.Tag = "EditPanel"; Grid.SetRow(editPanel, 4); // row 4 = 하단 버튼 행 (toolBar 추가로 1 증가) parentGrid.Children.Add(editPanel); _btnPanel.Margin = new Thickness(20, 0, 20, 16); textBox.Focus(); } } // ════════════════════════════════════════════════════════════ // 공통 버튼 팩토리 // ════════════════════════════════════════════════════════════ private static Border CreateMiniButton(string icon, Brush fg, Brush hoverBg) { var btn = new Border { Width = 24, Height = 24, CornerRadius = new CornerRadius(6), Background = Brushes.Transparent, Cursor = Cursors.Hand, Margin = new Thickness(1, 0, 1, 0), Child = new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 10, Foreground = fg, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }, }; btn.MouseEnter += (s, _) => ((Border)s).Background = hoverBg; btn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent; return btn; } private static Border CreateActionButton(string icon, string text, Brush borderColor, Brush textColor, bool filled) { var color = ((SolidColorBrush)borderColor).Color; var btn = new Border { CornerRadius = new CornerRadius(12), Padding = new Thickness(16, 8, 16, 8), Margin = new Thickness(4, 0, 4, 0), Cursor = Cursors.Hand, Background = filled ? borderColor : new SolidColorBrush(Color.FromArgb(0x18, color.R, color.G, color.B)), BorderBrush = filled ? Brushes.Transparent : new SolidColorBrush(Color.FromArgb(0x80, color.R, color.G, color.B)), BorderThickness = new Thickness(filled ? 0 : 1.2), }; var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = filled ? Brushes.White : textColor, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), }); sp.Children.Add(new TextBlock { Text = text, FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = filled ? Brushes.White : textColor, }); btn.Child = sp; btn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85; btn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0; return btn; } private static string ResolveUiExpressionLevel() { if (Application.Current is App app) return NormalizeUiExpressionLevel(app.SettingsService?.Settings?.Llm.AgentUiExpressionLevel); return "balanced"; } private static string NormalizeUiExpressionLevel(string? value) { return (value ?? "balanced").Trim().ToLowerInvariant() switch { "rich" => "rich", "simple" => "simple", _ => "balanced", }; } }