diff --git a/src/AxCopilot/Views/ChatWindow.Controls.cs b/src/AxCopilot/Views/ChatWindow.Controls.cs
index 9423c89..9ce104b 100644
--- a/src/AxCopilot/Views/ChatWindow.Controls.cs
+++ b/src/AxCopilot/Views/ChatWindow.Controls.cs
@@ -369,227 +369,4 @@ public partial class ChatWindow
MaximizeIcon.Text = WindowState == WindowState.Maximized ? "\uE923" : "\uE739"; // 복원/최대화 아이콘
}
private void BtnClose_Click(object sender, RoutedEventArgs e) => Close();
-
- // ─── 탭 전환 ──────────────────────────────────────────────────────────
-
- private string _activeTab = "Chat";
-
- private void SaveCurrentTabConversationId()
- {
- lock (_convLock)
- {
- if (_currentConversation != null && _currentConversation.Messages.Count > 0)
- {
- _tabConversationId[_activeTab] = _currentConversation.Id;
- // 탭 전환 시 현재 대화를 즉시 저장 (스트리밍 중이어도 진행 중인 내용 보존)
- try { _storage.Save(_currentConversation); } catch (Exception) { /* 대화 저장 실패 — UI 차단 방지 */ }
- }
- }
- // 탭별 마지막 대화 ID를 설정에 영속 저장 (앱 재시작 시 복원용)
- SaveLastConversations();
- }
-
- /// 탭 전환 전 스트리밍 중이면 즉시 중단합니다.
- private void StopStreamingIfActive()
- {
- if (!_isStreaming) return;
- // 스트리밍 중단
- _streamCts?.Cancel();
- _cursorTimer.Stop();
- _elapsedTimer.Stop();
- _typingTimer.Stop();
- StopRainbowGlow();
- HideStickyProgress();
- _activeStreamText = null;
- _elapsedLabel = null;
- _cachedStreamContent = "";
- _isStreaming = false;
- BtnSend.IsEnabled = true;
- BtnStop.Visibility = Visibility.Collapsed;
- BtnPause.Visibility = Visibility.Collapsed;
- PauseIcon.Text = "\uE769"; // 리셋
- BtnSend.Visibility = Visibility.Visible;
- _streamCts?.Dispose();
- _streamCts = null;
- SetStatusIdle();
- }
-
- private void TabChat_Checked(object sender, RoutedEventArgs e)
- {
- if (_activeTab == "Chat") return;
- StopStreamingIfActive();
- SaveCurrentTabConversationId();
- _activeTab = "Chat";
- _selectedCategory = ""; UpdateCategoryLabel();
- UpdateTabUI();
- UpdatePlanModeUI();
- }
-
- private void TabCowork_Checked(object sender, RoutedEventArgs e)
- {
- if (_activeTab == "Cowork") return;
- StopStreamingIfActive();
- SaveCurrentTabConversationId();
- _activeTab = "Cowork";
- _selectedCategory = ""; UpdateCategoryLabel();
- UpdateTabUI();
- UpdatePlanModeUI();
- }
-
- private void TabCode_Checked(object sender, RoutedEventArgs e)
- {
- if (_activeTab == "Code") return;
- StopStreamingIfActive();
- SaveCurrentTabConversationId();
- _activeTab = "Code";
- _selectedCategory = ""; UpdateCategoryLabel();
- UpdateTabUI();
- UpdatePlanModeUI();
- }
-
- /// 탭별로 마지막으로 활성화된 대화 ID를 기억.
- private readonly Dictionary _tabConversationId = new()
- {
- ["Chat"] = null, ["Cowork"] = null, ["Code"] = null,
- };
-
- private void UpdateTabUI()
- {
- // 폴더 바는 Cowork/Code 탭에서만 표시
- if (FolderBar != null)
- FolderBar.Visibility = _activeTab != "Chat" ? Visibility.Visible : Visibility.Collapsed;
-
- // 탭별 입력 안내 문구
- if (InputWatermark != null)
- {
- InputWatermark.Text = _activeTab switch
- {
- "Cowork" => "에이전트에게 작업을 요청하세요 (파일 읽기/쓰기, 문서 생성...)",
- "Code" => "코드 관련 작업을 요청하세요...",
- _ => _promptCardPlaceholder,
- };
- }
-
- // 권한 기본값 적용 (Cowork/Code 탭은 설정의 기본값 사용)
- ApplyTabDefaultPermission();
-
- // 포맷/디자인 드롭다운은 Cowork 탭에서만 표시
- if (_activeTab == "Cowork")
- {
- BuildBottomBar();
- if (Llm.ShowFileBrowser && FileBrowserPanel != null)
- {
- FileBrowserPanel.Visibility = Visibility.Visible;
- BuildFileTree();
- }
- }
- else if (_activeTab == "Code")
- {
- // Code 탭: 언어 선택기 + 파일 탐색기
- BuildCodeBottomBar();
- if (Llm.ShowFileBrowser && FileBrowserPanel != null)
- {
- FileBrowserPanel.Visibility = Visibility.Visible;
- BuildFileTree();
- }
- }
- else
- {
- MoodIconPanel.Children.Clear();
- if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Collapsed;
- if (FileBrowserPanel != null) FileBrowserPanel.Visibility = Visibility.Collapsed;
- }
-
- // 탭별 프리셋 버튼 재구성
- BuildTopicButtons();
-
- // 현재 대화를 해당 탭 대화로 전환
- SwitchToTabConversation();
-
- // Cowork/Code 탭 전환 시 팁 표시
- ShowRandomTip();
- }
-
- private void BtnPlanMode_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
- {
- // 3단 순환: off → auto → always → off
- Llm.PlanMode = Llm.PlanMode switch
- {
- "auto" => "always",
- "always" => "off",
- _ => "auto"
- };
- _settings.Save();
- UpdatePlanModeUI();
- }
-
- private void UpdatePlanModeUI()
- {
- var planMode = Llm.PlanMode ?? "off";
- if (PlanModeValue == null) return;
-
- PlanModeValue.Text = planMode switch
- {
- "auto" => "Auto",
- "always" => "Always",
- _ => "Off"
- };
- var isActive = planMode != "off";
- var activeBrush = ThemeResourceHelper.Accent(this);
- var secondaryBrush = ThemeResourceHelper.Secondary(this);
- if (PlanModeIcon != null) PlanModeIcon.Foreground = isActive ? activeBrush : secondaryBrush;
- if (PlanModeLabel != null) PlanModeLabel.Foreground = isActive ? activeBrush : secondaryBrush;
- if (BtnPlanMode != null)
- BtnPlanMode.Background = isActive
- ? new SolidColorBrush(Color.FromArgb(0x1A, 0x4B, 0x5E, 0xFC))
- : Brushes.Transparent;
- }
-
- private void SwitchToTabConversation()
- {
- // 이전 탭의 대화 저장
- lock (_convLock)
- {
- if (_currentConversation != null && _currentConversation.Messages.Count > 0)
- {
- try { _storage.Save(_currentConversation); } catch (Exception) { /* 대화 저장 실패 — UI 차단 방지 */ }
- }
- }
-
- // 현재 탭에 기억된 대화가 있으면 복원
- var savedId = _tabConversationId.GetValueOrDefault(_activeTab);
- if (!string.IsNullOrEmpty(savedId))
- {
- var conv = _storage.Load(savedId);
- if (conv != null)
- {
- if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab;
- lock (_convLock) _currentConversation = conv;
- MessagePanel.Children.Clear();
- foreach (var msg in conv.Messages)
- AddMessageBubble(msg.Role, msg.Content, animate: false, message: msg);
- EmptyState.Visibility = conv.Messages.Count > 0 ? Visibility.Collapsed : Visibility.Visible;
- UpdateChatTitle();
- RefreshConversationList();
- UpdateFolderBar();
- return;
- }
- }
-
- // 기억된 대화가 없으면 새 대화
- lock (_convLock)
- {
- _currentConversation = new ChatConversation { Tab = _activeTab };
- var workFolder = Llm.WorkFolder;
- if (!string.IsNullOrEmpty(workFolder) && _activeTab != "Chat")
- _currentConversation.WorkFolder = workFolder;
- }
- MessagePanel.Children.Clear();
- EmptyState.Visibility = Visibility.Visible;
- _attachedFiles.Clear();
- RefreshAttachedFilesUI();
- UpdateChatTitle();
- RefreshConversationList();
- UpdateFolderBar();
- }
}
diff --git a/src/AxCopilot/Views/ChatWindow.DropActions.cs b/src/AxCopilot/Views/ChatWindow.DropActions.cs
new file mode 100644
index 0000000..ed8e567
--- /dev/null
+++ b/src/AxCopilot/Views/ChatWindow.DropActions.cs
@@ -0,0 +1,183 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Input;
+using System.Windows.Media;
+
+namespace AxCopilot.Views;
+
+public partial class ChatWindow
+{
+ // ─── 드래그 앤 드롭 AI 액션 팝업 ─────────────────────────────────────
+
+ private static readonly Dictionary> DropActions = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["code"] =
+ [
+ ("코드 리뷰", "\uE943", "첨부된 코드를 리뷰해 주세요. 버그, 성능 이슈, 보안 취약점, 개선점을 찾아 구체적으로 제안하세요."),
+ ("코드 설명", "\uE946", "첨부된 코드를 상세히 설명해 주세요. 주요 함수, 데이터 흐름, 설계 패턴을 포함하세요."),
+ ("리팩토링 제안", "\uE70F", "첨부된 코드의 리팩토링 방안을 제안해 주세요. 가독성, 유지보수성, 성능을 고려하세요."),
+ ("테스트 생성", "\uE9D5", "첨부된 코드에 대한 단위 테스트 코드를 생성해 주세요."),
+ ],
+ ["document"] =
+ [
+ ("요약", "\uE8AB", "첨부된 문서를 핵심 포인트 위주로 간결하게 요약해 주세요."),
+ ("분석", "\uE9D9", "첨부된 문서의 내용을 분석하고 주요 인사이트를 도출해 주세요."),
+ ("번역", "\uE8C1", "첨부된 문서를 영어로 번역해 주세요. 원문의 톤과 뉘앙스를 유지하세요."),
+ ],
+ ["data"] =
+ [
+ ("데이터 분석", "\uE9D9", "첨부된 데이터를 분석해 주세요. 통계, 추세, 이상치를 찾아 보고하세요."),
+ ("시각화 제안", "\uE9D9", "첨부된 데이터를 시각화할 최적의 차트 유형을 제안하고 chart_create로 생성해 주세요."),
+ ("포맷 변환", "\uE8AB", "첨부된 데이터를 다른 형식으로 변환해 주세요. (CSV↔JSON↔Excel 등)"),
+ ],
+ ["image"] =
+ [
+ ("이미지 설명", "\uE946", "첨부된 이미지를 자세히 설명해 주세요. 내용, 레이아웃, 텍스트를 분석하세요."),
+ ("UI 리뷰", "\uE70F", "첨부된 UI 스크린샷을 리뷰해 주세요. UX 개선점, 접근성, 디자인 일관성을 평가하세요."),
+ ],
+ };
+
+ private static readonly HashSet CodeExtensions = new(StringComparer.OrdinalIgnoreCase)
+ { ".cs", ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".cpp", ".c", ".h", ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala", ".sh", ".ps1", ".bat", ".cmd", ".sql", ".xaml", ".vue" };
+ private static readonly HashSet DataExtensions = new(StringComparer.OrdinalIgnoreCase)
+ { ".csv", ".json", ".xml", ".yaml", ".yml", ".tsv" };
+ // ImageExtensions는 이미지 첨부 영역에서 정의됨 — 재사용
+
+ private Popup? _dropActionPopup;
+
+ private void ShowDropActionMenu(string[] files)
+ {
+ // 파일 유형 판별
+ var ext = System.IO.Path.GetExtension(files[0]).ToLowerInvariant();
+ string category;
+ if (CodeExtensions.Contains(ext)) category = "code";
+ else if (DataExtensions.Contains(ext)) category = "data";
+ else if (ImageExtensions.Contains(ext)) category = "image";
+ else category = "document";
+
+ var actions = DropActions.GetValueOrDefault(category) ?? DropActions["document"];
+
+ // 팝업 생성
+ _dropActionPopup?.SetValue(Popup.IsOpenProperty, false);
+
+ var panel = new StackPanel();
+ // 헤더
+ var header = new TextBlock
+ {
+ Text = $"📎 {System.IO.Path.GetFileName(files[0])}{(files.Length > 1 ? $" 외 {files.Length - 1}개" : "")}",
+ FontSize = 11,
+ Foreground = ThemeResourceHelper.Secondary(this),
+ Margin = new Thickness(12, 8, 12, 6),
+ };
+ panel.Children.Add(header);
+
+ // 액션 항목
+ var hoverBrush = ThemeResourceHelper.HoverBg(this);
+ var accentBrush = ThemeResourceHelper.Accent(this);
+ var textBrush = ThemeResourceHelper.Primary(this);
+
+ foreach (var (label, icon, prompt) in actions)
+ {
+ var capturedPrompt = prompt;
+ var row = new Border
+ {
+ Background = Brushes.Transparent,
+ CornerRadius = new CornerRadius(6),
+ Padding = new Thickness(12, 7, 12, 7),
+ Margin = new Thickness(4, 1, 4, 1),
+ Cursor = Cursors.Hand,
+ };
+ var stack = new StackPanel { Orientation = Orientation.Horizontal };
+ stack.Children.Add(new TextBlock
+ {
+ Text = icon,
+ FontFamily = ThemeResourceHelper.SegoeMdl2,
+ FontSize = 13, Foreground = accentBrush,
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(0, 0, 8, 0),
+ });
+ stack.Children.Add(new TextBlock
+ {
+ Text = label, FontSize = 13, FontWeight = FontWeights.SemiBold,
+ Foreground = textBrush, VerticalAlignment = VerticalAlignment.Center,
+ });
+ row.Child = stack;
+
+ row.MouseEnter += (_, _) => row.Background = hoverBrush;
+ row.MouseLeave += (_, _) => row.Background = Brushes.Transparent;
+ row.MouseLeftButtonUp += (_, _) =>
+ {
+ if (_dropActionPopup != null) _dropActionPopup.IsOpen = false;
+ foreach (var f in files) AddAttachedFile(f);
+ InputBox.Text = capturedPrompt;
+ InputBox.CaretIndex = InputBox.Text.Length;
+ InputBox.Focus();
+ if (Llm.DragDropAutoSend)
+ _ = SendMessageAsync();
+ };
+ panel.Children.Add(row);
+ }
+
+ // "첨부만" 항목
+ var attachOnly = new Border
+ {
+ Background = Brushes.Transparent,
+ CornerRadius = new CornerRadius(6),
+ Padding = new Thickness(12, 7, 12, 7),
+ Margin = new Thickness(4, 1, 4, 1),
+ Cursor = Cursors.Hand,
+ };
+ var attachStack = new StackPanel { Orientation = Orientation.Horizontal };
+ attachStack.Children.Add(new TextBlock
+ {
+ Text = "\uE723",
+ FontFamily = ThemeResourceHelper.SegoeMdl2,
+ FontSize = 13, Foreground = ThemeResourceHelper.Secondary(this),
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(0, 0, 8, 0),
+ });
+ attachStack.Children.Add(new TextBlock
+ {
+ Text = "첨부만", FontSize = 13,
+ Foreground = ThemeResourceHelper.Secondary(this),
+ VerticalAlignment = VerticalAlignment.Center,
+ });
+ attachOnly.Child = attachStack;
+ attachOnly.MouseEnter += (_, _) => attachOnly.Background = hoverBrush;
+ attachOnly.MouseLeave += (_, _) => attachOnly.Background = Brushes.Transparent;
+ attachOnly.MouseLeftButtonUp += (_, _) =>
+ {
+ if (_dropActionPopup != null) _dropActionPopup.IsOpen = false;
+ foreach (var f in files) AddAttachedFile(f);
+ InputBox.Focus();
+ };
+ panel.Children.Add(attachOnly);
+
+ var container = new Border
+ {
+ Background = ThemeResourceHelper.Background(this),
+ CornerRadius = new CornerRadius(12),
+ BorderBrush = ThemeResourceHelper.Border(this),
+ BorderThickness = new Thickness(1),
+ Padding = new Thickness(4, 4, 4, 6),
+ Child = panel,
+ MinWidth = 200,
+ Effect = new System.Windows.Media.Effects.DropShadowEffect
+ {
+ Color = Colors.Black, BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3,
+ },
+ };
+
+ _dropActionPopup = new Popup
+ {
+ PlacementTarget = InputBorder,
+ Placement = PlacementMode.Top,
+ StaysOpen = false,
+ AllowsTransparency = true,
+ PopupAnimation = PopupAnimation.Fade,
+ Child = container,
+ };
+ _dropActionPopup.IsOpen = true;
+ }
+}
diff --git a/src/AxCopilot/Views/ChatWindow.SlashCommands.cs b/src/AxCopilot/Views/ChatWindow.SlashCommands.cs
index 3e37a74..e1b0f62 100644
--- a/src/AxCopilot/Views/ChatWindow.SlashCommands.cs
+++ b/src/AxCopilot/Views/ChatWindow.SlashCommands.cs
@@ -403,177 +403,4 @@ public partial class ChatWindow
return (null, input);
}
-
- // ─── 드래그 앤 드롭 AI 액션 팝업 ─────────────────────────────────────
-
- private static readonly Dictionary> DropActions = new(StringComparer.OrdinalIgnoreCase)
- {
- ["code"] =
- [
- ("코드 리뷰", "\uE943", "첨부된 코드를 리뷰해 주세요. 버그, 성능 이슈, 보안 취약점, 개선점을 찾아 구체적으로 제안하세요."),
- ("코드 설명", "\uE946", "첨부된 코드를 상세히 설명해 주세요. 주요 함수, 데이터 흐름, 설계 패턴을 포함하세요."),
- ("리팩토링 제안", "\uE70F", "첨부된 코드의 리팩토링 방안을 제안해 주세요. 가독성, 유지보수성, 성능을 고려하세요."),
- ("테스트 생성", "\uE9D5", "첨부된 코드에 대한 단위 테스트 코드를 생성해 주세요."),
- ],
- ["document"] =
- [
- ("요약", "\uE8AB", "첨부된 문서를 핵심 포인트 위주로 간결하게 요약해 주세요."),
- ("분석", "\uE9D9", "첨부된 문서의 내용을 분석하고 주요 인사이트를 도출해 주세요."),
- ("번역", "\uE8C1", "첨부된 문서를 영어로 번역해 주세요. 원문의 톤과 뉘앙스를 유지하세요."),
- ],
- ["data"] =
- [
- ("데이터 분석", "\uE9D9", "첨부된 데이터를 분석해 주세요. 통계, 추세, 이상치를 찾아 보고하세요."),
- ("시각화 제안", "\uE9D9", "첨부된 데이터를 시각화할 최적의 차트 유형을 제안하고 chart_create로 생성해 주세요."),
- ("포맷 변환", "\uE8AB", "첨부된 데이터를 다른 형식으로 변환해 주세요. (CSV↔JSON↔Excel 등)"),
- ],
- ["image"] =
- [
- ("이미지 설명", "\uE946", "첨부된 이미지를 자세히 설명해 주세요. 내용, 레이아웃, 텍스트를 분석하세요."),
- ("UI 리뷰", "\uE70F", "첨부된 UI 스크린샷을 리뷰해 주세요. UX 개선점, 접근성, 디자인 일관성을 평가하세요."),
- ],
- };
-
- private static readonly HashSet CodeExtensions = new(StringComparer.OrdinalIgnoreCase)
- { ".cs", ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".cpp", ".c", ".h", ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala", ".sh", ".ps1", ".bat", ".cmd", ".sql", ".xaml", ".vue" };
- private static readonly HashSet DataExtensions = new(StringComparer.OrdinalIgnoreCase)
- { ".csv", ".json", ".xml", ".yaml", ".yml", ".tsv" };
- // ImageExtensions는 이미지 첨부 영역에서 정의됨 — 재사용
-
- private Popup? _dropActionPopup;
-
- private void ShowDropActionMenu(string[] files)
- {
- // 파일 유형 판별
- var ext = System.IO.Path.GetExtension(files[0]).ToLowerInvariant();
- string category;
- if (CodeExtensions.Contains(ext)) category = "code";
- else if (DataExtensions.Contains(ext)) category = "data";
- else if (ImageExtensions.Contains(ext)) category = "image";
- else category = "document";
-
- var actions = DropActions.GetValueOrDefault(category) ?? DropActions["document"];
-
- // 팝업 생성
- _dropActionPopup?.SetValue(Popup.IsOpenProperty, false);
-
- var panel = new StackPanel();
- // 헤더
- var header = new TextBlock
- {
- Text = $"📎 {System.IO.Path.GetFileName(files[0])}{(files.Length > 1 ? $" 외 {files.Length - 1}개" : "")}",
- FontSize = 11,
- Foreground = ThemeResourceHelper.Secondary(this),
- Margin = new Thickness(12, 8, 12, 6),
- };
- panel.Children.Add(header);
-
- // 액션 항목
- var hoverBrush = ThemeResourceHelper.HoverBg(this);
- var accentBrush = ThemeResourceHelper.Accent(this);
- var textBrush = ThemeResourceHelper.Primary(this);
-
- foreach (var (label, icon, prompt) in actions)
- {
- var capturedPrompt = prompt;
- var row = new Border
- {
- Background = Brushes.Transparent,
- CornerRadius = new CornerRadius(6),
- Padding = new Thickness(12, 7, 12, 7),
- Margin = new Thickness(4, 1, 4, 1),
- Cursor = Cursors.Hand,
- };
- var stack = new StackPanel { Orientation = Orientation.Horizontal };
- stack.Children.Add(new TextBlock
- {
- Text = icon,
- FontFamily = ThemeResourceHelper.SegoeMdl2,
- FontSize = 13, Foreground = accentBrush,
- VerticalAlignment = VerticalAlignment.Center,
- Margin = new Thickness(0, 0, 8, 0),
- });
- stack.Children.Add(new TextBlock
- {
- Text = label, FontSize = 13, FontWeight = FontWeights.SemiBold,
- Foreground = textBrush, VerticalAlignment = VerticalAlignment.Center,
- });
- row.Child = stack;
-
- row.MouseEnter += (_, _) => row.Background = hoverBrush;
- row.MouseLeave += (_, _) => row.Background = Brushes.Transparent;
- row.MouseLeftButtonUp += (_, _) =>
- {
- if (_dropActionPopup != null) _dropActionPopup.IsOpen = false;
- foreach (var f in files) AddAttachedFile(f);
- InputBox.Text = capturedPrompt;
- InputBox.CaretIndex = InputBox.Text.Length;
- InputBox.Focus();
- if (Llm.DragDropAutoSend)
- _ = SendMessageAsync();
- };
- panel.Children.Add(row);
- }
-
- // "첨부만" 항목
- var attachOnly = new Border
- {
- Background = Brushes.Transparent,
- CornerRadius = new CornerRadius(6),
- Padding = new Thickness(12, 7, 12, 7),
- Margin = new Thickness(4, 1, 4, 1),
- Cursor = Cursors.Hand,
- };
- var attachStack = new StackPanel { Orientation = Orientation.Horizontal };
- attachStack.Children.Add(new TextBlock
- {
- Text = "\uE723",
- FontFamily = ThemeResourceHelper.SegoeMdl2,
- FontSize = 13, Foreground = ThemeResourceHelper.Secondary(this),
- VerticalAlignment = VerticalAlignment.Center,
- Margin = new Thickness(0, 0, 8, 0),
- });
- attachStack.Children.Add(new TextBlock
- {
- Text = "첨부만", FontSize = 13,
- Foreground = ThemeResourceHelper.Secondary(this),
- VerticalAlignment = VerticalAlignment.Center,
- });
- attachOnly.Child = attachStack;
- attachOnly.MouseEnter += (_, _) => attachOnly.Background = hoverBrush;
- attachOnly.MouseLeave += (_, _) => attachOnly.Background = Brushes.Transparent;
- attachOnly.MouseLeftButtonUp += (_, _) =>
- {
- if (_dropActionPopup != null) _dropActionPopup.IsOpen = false;
- foreach (var f in files) AddAttachedFile(f);
- InputBox.Focus();
- };
- panel.Children.Add(attachOnly);
-
- var container = new Border
- {
- Background = ThemeResourceHelper.Background(this),
- CornerRadius = new CornerRadius(12),
- BorderBrush = ThemeResourceHelper.Border(this),
- BorderThickness = new Thickness(1),
- Padding = new Thickness(4, 4, 4, 6),
- Child = panel,
- MinWidth = 200,
- Effect = new System.Windows.Media.Effects.DropShadowEffect
- {
- Color = Colors.Black, BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3,
- },
- };
-
- _dropActionPopup = new Popup
- {
- PlacementTarget = InputBorder,
- Placement = PlacementMode.Top,
- StaysOpen = false,
- AllowsTransparency = true,
- PopupAnimation = PopupAnimation.Fade,
- Child = container,
- };
- _dropActionPopup.IsOpen = true;
- }
}
diff --git a/src/AxCopilot/Views/ChatWindow.TabSwitching.cs b/src/AxCopilot/Views/ChatWindow.TabSwitching.cs
new file mode 100644
index 0000000..e2a4e07
--- /dev/null
+++ b/src/AxCopilot/Views/ChatWindow.TabSwitching.cs
@@ -0,0 +1,232 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using AxCopilot.Services;
+
+namespace AxCopilot.Views;
+
+public partial class ChatWindow
+{
+ // ─── 탭 전환 ──────────────────────────────────────────────────────────
+
+ private string _activeTab = "Chat";
+
+ /// 탭별로 마지막으로 활성화된 대화 ID를 기억.
+ private readonly Dictionary _tabConversationId = new()
+ {
+ ["Chat"] = null, ["Cowork"] = null, ["Code"] = null,
+ };
+
+ private void SaveCurrentTabConversationId()
+ {
+ lock (_convLock)
+ {
+ if (_currentConversation != null && _currentConversation.Messages.Count > 0)
+ {
+ _tabConversationId[_activeTab] = _currentConversation.Id;
+ // 탭 전환 시 현재 대화를 즉시 저장 (스트리밍 중이어도 진행 중인 내용 보존)
+ try { _storage.Save(_currentConversation); } catch (Exception) { /* 대화 저장 실패 — UI 차단 방지 */ }
+ }
+ }
+ // 탭별 마지막 대화 ID를 설정에 영속 저장 (앱 재시작 시 복원용)
+ SaveLastConversations();
+ }
+
+ /// 탭 전환 전 스트리밍 중이면 즉시 중단합니다.
+ private void StopStreamingIfActive()
+ {
+ if (!_isStreaming) return;
+ // 스트리밍 중단
+ _streamCts?.Cancel();
+ _cursorTimer.Stop();
+ _elapsedTimer.Stop();
+ _typingTimer.Stop();
+ StopRainbowGlow();
+ HideStickyProgress();
+ _activeStreamText = null;
+ _elapsedLabel = null;
+ _cachedStreamContent = "";
+ _isStreaming = false;
+ BtnSend.IsEnabled = true;
+ BtnStop.Visibility = Visibility.Collapsed;
+ BtnPause.Visibility = Visibility.Collapsed;
+ PauseIcon.Text = "\uE769"; // 리셋
+ BtnSend.Visibility = Visibility.Visible;
+ _streamCts?.Dispose();
+ _streamCts = null;
+ SetStatusIdle();
+ }
+
+ private void TabChat_Checked(object sender, RoutedEventArgs e)
+ {
+ if (_activeTab == "Chat") return;
+ StopStreamingIfActive();
+ SaveCurrentTabConversationId();
+ _activeTab = "Chat";
+ _selectedCategory = ""; UpdateCategoryLabel();
+ UpdateTabUI();
+ UpdatePlanModeUI();
+ }
+
+ private void TabCowork_Checked(object sender, RoutedEventArgs e)
+ {
+ if (_activeTab == "Cowork") return;
+ StopStreamingIfActive();
+ SaveCurrentTabConversationId();
+ _activeTab = "Cowork";
+ _selectedCategory = ""; UpdateCategoryLabel();
+ UpdateTabUI();
+ UpdatePlanModeUI();
+ }
+
+ private void TabCode_Checked(object sender, RoutedEventArgs e)
+ {
+ if (_activeTab == "Code") return;
+ StopStreamingIfActive();
+ SaveCurrentTabConversationId();
+ _activeTab = "Code";
+ _selectedCategory = ""; UpdateCategoryLabel();
+ UpdateTabUI();
+ UpdatePlanModeUI();
+ }
+
+ private void UpdateTabUI()
+ {
+ // 폴더 바는 Cowork/Code 탭에서만 표시
+ if (FolderBar != null)
+ FolderBar.Visibility = _activeTab != "Chat" ? Visibility.Visible : Visibility.Collapsed;
+
+ // 탭별 입력 안내 문구
+ if (InputWatermark != null)
+ {
+ InputWatermark.Text = _activeTab switch
+ {
+ "Cowork" => "에이전트에게 작업을 요청하세요 (파일 읽기/쓰기, 문서 생성...)",
+ "Code" => "코드 관련 작업을 요청하세요...",
+ _ => _promptCardPlaceholder,
+ };
+ }
+
+ // 권한 기본값 적용 (Cowork/Code 탭은 설정의 기본값 사용)
+ ApplyTabDefaultPermission();
+
+ // 포맷/디자인 드롭다운은 Cowork 탭에서만 표시
+ if (_activeTab == "Cowork")
+ {
+ BuildBottomBar();
+ if (Llm.ShowFileBrowser && FileBrowserPanel != null)
+ {
+ FileBrowserPanel.Visibility = Visibility.Visible;
+ BuildFileTree();
+ }
+ }
+ else if (_activeTab == "Code")
+ {
+ // Code 탭: 언어 선택기 + 파일 탐색기
+ BuildCodeBottomBar();
+ if (Llm.ShowFileBrowser && FileBrowserPanel != null)
+ {
+ FileBrowserPanel.Visibility = Visibility.Visible;
+ BuildFileTree();
+ }
+ }
+ else
+ {
+ MoodIconPanel.Children.Clear();
+ if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Collapsed;
+ if (FileBrowserPanel != null) FileBrowserPanel.Visibility = Visibility.Collapsed;
+ }
+
+ // 탭별 프리셋 버튼 재구성
+ BuildTopicButtons();
+
+ // 현재 대화를 해당 탭 대화로 전환
+ SwitchToTabConversation();
+
+ // Cowork/Code 탭 전환 시 팁 표시
+ ShowRandomTip();
+ }
+
+ private void BtnPlanMode_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ // 3단 순환: off → auto → always → off
+ Llm.PlanMode = Llm.PlanMode switch
+ {
+ "auto" => "always",
+ "always" => "off",
+ _ => "auto"
+ };
+ _settings.Save();
+ UpdatePlanModeUI();
+ }
+
+ private void UpdatePlanModeUI()
+ {
+ var planMode = Llm.PlanMode ?? "off";
+ if (PlanModeValue == null) return;
+
+ PlanModeValue.Text = planMode switch
+ {
+ "auto" => "Auto",
+ "always" => "Always",
+ _ => "Off"
+ };
+ var isActive = planMode != "off";
+ var activeBrush = ThemeResourceHelper.Accent(this);
+ var secondaryBrush = ThemeResourceHelper.Secondary(this);
+ if (PlanModeIcon != null) PlanModeIcon.Foreground = isActive ? activeBrush : secondaryBrush;
+ if (PlanModeLabel != null) PlanModeLabel.Foreground = isActive ? activeBrush : secondaryBrush;
+ if (BtnPlanMode != null)
+ BtnPlanMode.Background = isActive
+ ? new SolidColorBrush(Color.FromArgb(0x1A, 0x4B, 0x5E, 0xFC))
+ : Brushes.Transparent;
+ }
+
+ private void SwitchToTabConversation()
+ {
+ // 이전 탭의 대화 저장
+ lock (_convLock)
+ {
+ if (_currentConversation != null && _currentConversation.Messages.Count > 0)
+ {
+ try { _storage.Save(_currentConversation); } catch (Exception) { /* 대화 저장 실패 — UI 차단 방지 */ }
+ }
+ }
+
+ // 현재 탭에 기억된 대화가 있으면 복원
+ var savedId = _tabConversationId.GetValueOrDefault(_activeTab);
+ if (!string.IsNullOrEmpty(savedId))
+ {
+ var conv = _storage.Load(savedId);
+ if (conv != null)
+ {
+ if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab;
+ lock (_convLock) _currentConversation = conv;
+ MessagePanel.Children.Clear();
+ foreach (var msg in conv.Messages)
+ AddMessageBubble(msg.Role, msg.Content, animate: false, message: msg);
+ EmptyState.Visibility = conv.Messages.Count > 0 ? Visibility.Collapsed : Visibility.Visible;
+ UpdateChatTitle();
+ RefreshConversationList();
+ UpdateFolderBar();
+ return;
+ }
+ }
+
+ // 기억된 대화가 없으면 새 대화
+ lock (_convLock)
+ {
+ _currentConversation = new Models.ChatConversation { Tab = _activeTab };
+ var workFolder = Llm.WorkFolder;
+ if (!string.IsNullOrEmpty(workFolder) && _activeTab != "Chat")
+ _currentConversation.WorkFolder = workFolder;
+ }
+ MessagePanel.Children.Clear();
+ EmptyState.Visibility = Visibility.Visible;
+ _attachedFiles.Clear();
+ RefreshAttachedFilesUI();
+ UpdateChatTitle();
+ RefreshConversationList();
+ UpdateFolderBar();
+ }
+}
diff --git a/src/AxCopilot/Views/SkillGalleryWindow.SkillDetail.cs b/src/AxCopilot/Views/SkillGalleryWindow.SkillDetail.cs
new file mode 100644
index 0000000..78aff17
--- /dev/null
+++ b/src/AxCopilot/Views/SkillGalleryWindow.SkillDetail.cs
@@ -0,0 +1,210 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using AxCopilot.Services.Agent;
+
+namespace AxCopilot.Views;
+
+public partial class SkillGalleryWindow
+{
+ // ─── 스킬 상세 보기 팝업 ───────────────────────────────────────────────
+
+ private void ShowSkillDetail(SkillDefinition skill)
+ {
+ var bgBrush = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black;
+ var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
+ var subBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
+ var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent;
+ var borderBr = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
+
+ var popup = new Window
+ {
+ Title = $"/{skill.Name}",
+ Width = 580,
+ Height = 480,
+ WindowStyle = WindowStyle.None,
+ AllowsTransparency = true,
+ Background = Brushes.Transparent,
+ WindowStartupLocation = WindowStartupLocation.CenterOwner,
+ Owner = this,
+ };
+
+ var outerBorder = new Border
+ {
+ Background = bgBrush,
+ CornerRadius = new CornerRadius(12),
+ BorderBrush = borderBr,
+ BorderThickness = new Thickness(1, 1, 1, 1),
+ Effect = new System.Windows.Media.Effects.DropShadowEffect
+ {
+ BlurRadius = 20,
+ ShadowDepth = 4,
+ Opacity = 0.3,
+ Color = Colors.Black,
+ },
+ };
+
+ var mainGrid = new Grid();
+ mainGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(44) });
+ mainGrid.RowDefinitions.Add(new RowDefinition());
+
+ // ── 타이틀바 ──
+ var titleBar = new Border
+ {
+ CornerRadius = new CornerRadius(12, 12, 0, 0),
+ Background = itemBg,
+ };
+ titleBar.MouseLeftButtonDown += (_, e) =>
+ {
+ var pos = e.GetPosition(popup);
+ if (pos.X > popup.ActualWidth - 50) return;
+ popup.DragMove();
+ };
+
+ var titleGrid = new Grid { Margin = new Thickness(16, 0, 12, 0) };
+ var titleLeft = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
+ titleLeft.Children.Add(new TextBlock
+ {
+ Text = skill.Icon,
+ FontFamily = ThemeResourceHelper.SegoeMdl2,
+ FontSize = 14,
+ Foreground = new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)),
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(0, 1, 8, 0),
+ });
+ titleLeft.Children.Add(new TextBlock
+ {
+ Text = $"/{skill.Name} {skill.Label}",
+ FontSize = 14,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = fgBrush,
+ VerticalAlignment = VerticalAlignment.Center,
+ });
+ titleGrid.Children.Add(titleLeft);
+
+ var closeBtn = new Border
+ {
+ Width = 28, Height = 28,
+ CornerRadius = new CornerRadius(6),
+ Cursor = Cursors.Hand,
+ HorizontalAlignment = HorizontalAlignment.Right,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ closeBtn.Child = new TextBlock
+ {
+ Text = "\uE8BB",
+ FontFamily = ThemeResourceHelper.SegoeMdl2,
+ FontSize = 10,
+ Foreground = subBrush,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ closeBtn.MouseEnter += (s, _) => ((Border)s).Background =
+ new SolidColorBrush(Color.FromArgb(0x33, 0xFF, 0x44, 0x44));
+ closeBtn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
+ closeBtn.MouseLeftButtonUp += (_, _) => popup.Close();
+ titleGrid.Children.Add(closeBtn);
+
+ titleBar.Child = titleGrid;
+ Grid.SetRow(titleBar, 0);
+
+ // ── 본문: 스킬 정보 + 프롬프트 미리보기 ──
+ var body = new ScrollViewer
+ {
+ VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
+ Padding = new Thickness(20, 16, 20, 16),
+ };
+
+ var bodyPanel = new StackPanel();
+
+ // 메타 정보
+ var metaGrid = new Grid { Margin = new Thickness(0, 0, 0, 14) };
+ metaGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(80) });
+ metaGrid.ColumnDefinitions.Add(new ColumnDefinition());
+
+ void AddMetaRow(string label, string value, int row)
+ {
+ if (string.IsNullOrEmpty(value)) return;
+ metaGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ var lb = new TextBlock
+ {
+ Text = label, FontSize = 11.5, Foreground = subBrush,
+ Margin = new Thickness(0, 2, 0, 2),
+ };
+ Grid.SetRow(lb, row); Grid.SetColumn(lb, 0);
+ metaGrid.Children.Add(lb);
+
+ var vb = new TextBlock
+ {
+ Text = value, FontSize = 11.5, Foreground = fgBrush,
+ TextWrapping = TextWrapping.Wrap,
+ Margin = new Thickness(0, 2, 0, 2),
+ };
+ Grid.SetRow(vb, row); Grid.SetColumn(vb, 1);
+ metaGrid.Children.Add(vb);
+ }
+
+ var metaRow = 0;
+ AddMetaRow("명령어", $"/{skill.Name}", metaRow++);
+ AddMetaRow("라벨", skill.Label, metaRow++);
+ AddMetaRow("설명", skill.Description, metaRow++);
+ if (!string.IsNullOrEmpty(skill.Requires))
+ AddMetaRow("런타임", skill.Requires, metaRow++);
+ if (!string.IsNullOrEmpty(skill.AllowedTools))
+ AddMetaRow("허용 도구", skill.AllowedTools, metaRow++);
+ AddMetaRow("상태", skill.IsAvailable ? "✓ 사용 가능" : $"✗ {skill.UnavailableHint}", metaRow++);
+ AddMetaRow("경로", skill.FilePath, metaRow++);
+
+ bodyPanel.Children.Add(metaGrid);
+
+ // 구분선
+ bodyPanel.Children.Add(new Border
+ {
+ Height = 1,
+ Background = borderBr,
+ Margin = new Thickness(0, 4, 0, 12),
+ });
+
+ // 프롬프트 내용 미리보기
+ bodyPanel.Children.Add(new TextBlock
+ {
+ Text = "시스템 프롬프트 (미리보기)",
+ FontSize = 11,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = subBrush,
+ Margin = new Thickness(0, 0, 0, 6),
+ });
+
+ var promptBorder = new Border
+ {
+ Background = itemBg,
+ CornerRadius = new CornerRadius(8),
+ Padding = new Thickness(12, 10, 12, 10),
+ };
+ var promptText = skill.SystemPrompt;
+ if (promptText.Length > 2000)
+ promptText = promptText[..2000] + "\n\n... (이하 생략)";
+
+ promptBorder.Child = new TextBlock
+ {
+ Text = promptText,
+ FontSize = 11.5,
+ FontFamily = ThemeResourceHelper.ConsolasCode,
+ Foreground = fgBrush,
+ TextWrapping = TextWrapping.Wrap,
+ Opacity = 0.85,
+ };
+ bodyPanel.Children.Add(promptBorder);
+
+ body.Content = bodyPanel;
+ Grid.SetRow(body, 1);
+
+ mainGrid.Children.Add(titleBar);
+ mainGrid.Children.Add(body);
+ outerBorder.Child = mainGrid;
+ popup.Content = outerBorder;
+
+ popup.ShowDialog();
+ }
+}
diff --git a/src/AxCopilot/Views/SkillGalleryWindow.xaml.cs b/src/AxCopilot/Views/SkillGalleryWindow.xaml.cs
index 0fbda14..f61ce85 100644
--- a/src/AxCopilot/Views/SkillGalleryWindow.xaml.cs
+++ b/src/AxCopilot/Views/SkillGalleryWindow.xaml.cs
@@ -428,204 +428,4 @@ public partial class SkillGalleryWindow : Window
btn.MouseLeftButtonUp += (_, _) => action();
return btn;
}
-
- // ─── 스킬 상세 보기 팝업 ───────────────────────────────────────────────
-
- private void ShowSkillDetail(SkillDefinition skill)
- {
- var bgBrush = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black;
- var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
- var subBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
- var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent;
- var borderBr = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
-
- var popup = new Window
- {
- Title = $"/{skill.Name}",
- Width = 580,
- Height = 480,
- WindowStyle = WindowStyle.None,
- AllowsTransparency = true,
- Background = Brushes.Transparent,
- WindowStartupLocation = WindowStartupLocation.CenterOwner,
- Owner = this,
- };
-
- var outerBorder = new Border
- {
- Background = bgBrush,
- CornerRadius = new CornerRadius(12),
- BorderBrush = borderBr,
- BorderThickness = new Thickness(1, 1, 1, 1),
- Effect = new System.Windows.Media.Effects.DropShadowEffect
- {
- BlurRadius = 20,
- ShadowDepth = 4,
- Opacity = 0.3,
- Color = Colors.Black,
- },
- };
-
- var mainGrid = new Grid();
- mainGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(44) });
- mainGrid.RowDefinitions.Add(new RowDefinition());
-
- // ── 타이틀바 ──
- var titleBar = new Border
- {
- CornerRadius = new CornerRadius(12, 12, 0, 0),
- Background = itemBg,
- };
- titleBar.MouseLeftButtonDown += (_, e) =>
- {
- var pos = e.GetPosition(popup);
- if (pos.X > popup.ActualWidth - 50) return;
- popup.DragMove();
- };
-
- var titleGrid = new Grid { Margin = new Thickness(16, 0, 12, 0) };
- var titleLeft = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
- titleLeft.Children.Add(new TextBlock
- {
- Text = skill.Icon,
- FontFamily = ThemeResourceHelper.SegoeMdl2,
- FontSize = 14,
- Foreground = new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)),
- VerticalAlignment = VerticalAlignment.Center,
- Margin = new Thickness(0, 1, 8, 0),
- });
- titleLeft.Children.Add(new TextBlock
- {
- Text = $"/{skill.Name} {skill.Label}",
- FontSize = 14,
- FontWeight = FontWeights.SemiBold,
- Foreground = fgBrush,
- VerticalAlignment = VerticalAlignment.Center,
- });
- titleGrid.Children.Add(titleLeft);
-
- var closeBtn = new Border
- {
- Width = 28, Height = 28,
- CornerRadius = new CornerRadius(6),
- Cursor = Cursors.Hand,
- HorizontalAlignment = HorizontalAlignment.Right,
- VerticalAlignment = VerticalAlignment.Center,
- };
- closeBtn.Child = new TextBlock
- {
- Text = "\uE8BB",
- FontFamily = ThemeResourceHelper.SegoeMdl2,
- FontSize = 10,
- Foreground = subBrush,
- HorizontalAlignment = HorizontalAlignment.Center,
- VerticalAlignment = VerticalAlignment.Center,
- };
- closeBtn.MouseEnter += (s, _) => ((Border)s).Background =
- new SolidColorBrush(Color.FromArgb(0x33, 0xFF, 0x44, 0x44));
- closeBtn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
- closeBtn.MouseLeftButtonUp += (_, _) => popup.Close();
- titleGrid.Children.Add(closeBtn);
-
- titleBar.Child = titleGrid;
- Grid.SetRow(titleBar, 0);
-
- // ── 본문: 스킬 정보 + 프롬프트 미리보기 ──
- var body = new ScrollViewer
- {
- VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
- Padding = new Thickness(20, 16, 20, 16),
- };
-
- var bodyPanel = new StackPanel();
-
- // 메타 정보
- var metaGrid = new Grid { Margin = new Thickness(0, 0, 0, 14) };
- metaGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(80) });
- metaGrid.ColumnDefinitions.Add(new ColumnDefinition());
-
- void AddMetaRow(string label, string value, int row)
- {
- if (string.IsNullOrEmpty(value)) return;
- metaGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
- var lb = new TextBlock
- {
- Text = label, FontSize = 11.5, Foreground = subBrush,
- Margin = new Thickness(0, 2, 0, 2),
- };
- Grid.SetRow(lb, row); Grid.SetColumn(lb, 0);
- metaGrid.Children.Add(lb);
-
- var vb = new TextBlock
- {
- Text = value, FontSize = 11.5, Foreground = fgBrush,
- TextWrapping = TextWrapping.Wrap,
- Margin = new Thickness(0, 2, 0, 2),
- };
- Grid.SetRow(vb, row); Grid.SetColumn(vb, 1);
- metaGrid.Children.Add(vb);
- }
-
- var metaRow = 0;
- AddMetaRow("명령어", $"/{skill.Name}", metaRow++);
- AddMetaRow("라벨", skill.Label, metaRow++);
- AddMetaRow("설명", skill.Description, metaRow++);
- if (!string.IsNullOrEmpty(skill.Requires))
- AddMetaRow("런타임", skill.Requires, metaRow++);
- if (!string.IsNullOrEmpty(skill.AllowedTools))
- AddMetaRow("허용 도구", skill.AllowedTools, metaRow++);
- AddMetaRow("상태", skill.IsAvailable ? "✓ 사용 가능" : $"✗ {skill.UnavailableHint}", metaRow++);
- AddMetaRow("경로", skill.FilePath, metaRow++);
-
- bodyPanel.Children.Add(metaGrid);
-
- // 구분선
- bodyPanel.Children.Add(new Border
- {
- Height = 1,
- Background = borderBr,
- Margin = new Thickness(0, 4, 0, 12),
- });
-
- // 프롬프트 내용 미리보기
- bodyPanel.Children.Add(new TextBlock
- {
- Text = "시스템 프롬프트 (미리보기)",
- FontSize = 11,
- FontWeight = FontWeights.SemiBold,
- Foreground = subBrush,
- Margin = new Thickness(0, 0, 0, 6),
- });
-
- var promptBorder = new Border
- {
- Background = itemBg,
- CornerRadius = new CornerRadius(8),
- Padding = new Thickness(12, 10, 12, 10),
- };
- var promptText = skill.SystemPrompt;
- if (promptText.Length > 2000)
- promptText = promptText[..2000] + "\n\n... (이하 생략)";
-
- promptBorder.Child = new TextBlock
- {
- Text = promptText,
- FontSize = 11.5,
- FontFamily = ThemeResourceHelper.ConsolasCode,
- Foreground = fgBrush,
- TextWrapping = TextWrapping.Wrap,
- Opacity = 0.85,
- };
- bodyPanel.Children.Add(promptBorder);
-
- body.Content = bodyPanel;
- Grid.SetRow(body, 1);
-
- mainGrid.Children.Add(titleBar);
- mainGrid.Children.Add(body);
- outerBorder.Child = mainGrid;
- popup.Content = outerBorder;
-
- popup.ShowDialog();
- }
}
diff --git a/src/AxCopilot/Views/WorkflowAnalyzerWindow.Charts.cs b/src/AxCopilot/Views/WorkflowAnalyzerWindow.Charts.cs
index 8b93ab0..aa8cdb3 100644
--- a/src/AxCopilot/Views/WorkflowAnalyzerWindow.Charts.cs
+++ b/src/AxCopilot/Views/WorkflowAnalyzerWindow.Charts.cs
@@ -394,274 +394,4 @@ public partial class WorkflowAnalyzerWindow
});
return sp;
}
-
- // ─── 타임라인 노드 생성 ────────────────────────────────────────
-
- private Border CreateTimelineNode(AgentEvent evt)
- {
- var (icon, iconColor, label) = GetEventVisual(evt);
-
- var node = new Border
- {
- Background = Brushes.Transparent,
- Margin = new Thickness(0, 1, 0, 1),
- Padding = new Thickness(8, 6, 8, 6),
- CornerRadius = new CornerRadius(8),
- Cursor = Cursors.Hand,
- Tag = evt,
- };
-
- var grid = new Grid();
- grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(28) });
- grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(3) });
- grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
-
- // 아이콘 원
- var iconBorder = new Border
- {
- Width = 22, Height = 22,
- CornerRadius = new CornerRadius(11),
- Background = new SolidColorBrush(iconColor),
- HorizontalAlignment = HorizontalAlignment.Center,
- VerticalAlignment = VerticalAlignment.Top,
- Margin = new Thickness(0, 2, 0, 0),
- };
- iconBorder.Child = new TextBlock
- {
- Text = icon,
- FontFamily = ThemeResourceHelper.SegoeMdl2,
- FontSize = 10,
- Foreground = Brushes.White,
- HorizontalAlignment = HorizontalAlignment.Center,
- VerticalAlignment = VerticalAlignment.Center,
- };
- Grid.SetColumn(iconBorder, 0);
- grid.Children.Add(iconBorder);
-
- // 타임라인 세로 선
- var line = new Rectangle
- {
- Width = 2,
- Fill = TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
- HorizontalAlignment = HorizontalAlignment.Center,
- VerticalAlignment = VerticalAlignment.Stretch,
- Opacity = 0.4,
- };
- Grid.SetColumn(line, 1);
- grid.Children.Add(line);
-
- // 내용
- var content = new StackPanel { Margin = new Thickness(8, 0, 0, 0) };
-
- var headerPanel = new StackPanel { Orientation = Orientation.Horizontal };
- headerPanel.Children.Add(new TextBlock
- {
- Text = label,
- FontSize = 12,
- FontWeight = FontWeights.SemiBold,
- Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
- VerticalAlignment = VerticalAlignment.Center,
- });
-
- if (evt.ElapsedMs > 0)
- {
- headerPanel.Children.Add(new TextBlock
- {
- Text = evt.ElapsedMs >= 1000 ? $" {evt.ElapsedMs / 1000.0:F1}s" : $" {evt.ElapsedMs}ms",
- FontSize = 10,
- Foreground = evt.ElapsedMs > 3000
- ? Brushes.OrangeRed
- : TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
- VerticalAlignment = VerticalAlignment.Center,
- });
- }
-
- headerPanel.Children.Add(new TextBlock
- {
- Text = $" {evt.Timestamp:HH:mm:ss}",
- FontSize = 9,
- Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
- VerticalAlignment = VerticalAlignment.Center,
- Opacity = 0.6,
- });
- content.Children.Add(headerPanel);
-
- if (!string.IsNullOrEmpty(evt.Summary))
- {
- content.Children.Add(new TextBlock
- {
- Text = Truncate(evt.Summary, 120),
- FontSize = 11,
- Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
- TextWrapping = TextWrapping.Wrap,
- Margin = new Thickness(0, 2, 0, 0),
- LineHeight = 16,
- });
- }
-
- if (evt.InputTokens > 0 || evt.OutputTokens > 0)
- {
- var tokenPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 0) };
- tokenPanel.Children.Add(CreateBadge($"↑{evt.InputTokens:#,0}", "#3B82F6"));
- tokenPanel.Children.Add(CreateBadge($"↓{evt.OutputTokens:#,0}", "#10B981"));
- content.Children.Add(tokenPanel);
- }
-
- Grid.SetColumn(content, 2);
- grid.Children.Add(content);
- node.Child = grid;
-
- var hoverBrush = TryFindResource("ItemHoverBackground") as Brush
- ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
- node.MouseEnter += (_, _) => node.Background = hoverBrush;
- node.MouseLeave += (_, _) => node.Background = Brushes.Transparent;
- node.MouseLeftButtonUp += (_, _) => ShowDetail(evt);
-
- return node;
- }
-
- private static (string Icon, Color Color, string Label) GetEventVisual(AgentEvent evt)
- {
- return evt.Type switch
- {
- AgentEventType.Thinking => ("\uE7BA", Color.FromRgb(0x60, 0xA5, 0xFA), "사고"),
- AgentEventType.Planning => ("\uE9D5", Color.FromRgb(0xA7, 0x8B, 0xFA), "계획"),
- AgentEventType.StepStart => ("\uE72A", Color.FromRgb(0x60, 0xA5, 0xFA), $"단계 {evt.StepCurrent}/{evt.StepTotal}"),
- AgentEventType.StepDone => ("\uE73E", Color.FromRgb(0x34, 0xD3, 0x99), $"단계 완료"),
- AgentEventType.ToolCall => ("\uE756", Color.FromRgb(0xFB, 0xBF, 0x24), evt.ToolName),
- AgentEventType.ToolResult=> ("\uE73E", Color.FromRgb(0x34, 0xD3, 0x99), $"{evt.ToolName} ✓"),
- AgentEventType.SkillCall => ("\uE768", Color.FromRgb(0xA7, 0x8B, 0xFA), $"스킬: {evt.ToolName}"),
- AgentEventType.Error => ("\uE783", Color.FromRgb(0xF8, 0x71, 0x71), $"{evt.ToolName} ✗"),
- AgentEventType.Complete => ("\uE930", Color.FromRgb(0x34, 0xD3, 0x99), "완료"),
- AgentEventType.Decision => ("\uE8C8", Color.FromRgb(0xFB, 0xBF, 0x24), "의사결정"),
- _ => ("\uE946", Color.FromRgb(0x6B, 0x72, 0x80), "이벤트"),
- };
- }
-
- private static Border CreateBadge(string text, string colorHex)
- {
- var color = (Color)ColorConverter.ConvertFromString(colorHex);
- return new Border
- {
- Background = new SolidColorBrush(Color.FromArgb(0x30, color.R, color.G, color.B)),
- CornerRadius = new CornerRadius(4),
- Padding = new Thickness(5, 1, 5, 1),
- Margin = new Thickness(0, 0, 4, 0),
- Child = new TextBlock
- {
- Text = text,
- FontSize = 9,
- Foreground = new SolidColorBrush(color),
- FontWeight = FontWeights.SemiBold,
- },
- };
- }
-
- // ─── 상세 패널 ─────────────────────────────────────────────
-
- private void ShowDetail(AgentEvent evt)
- {
- DetailPanel.Visibility = Visibility.Visible;
- DetailPanel.Tag = evt;
- var (_, color, label) = GetEventVisual(evt);
-
- DetailTitle.Text = label;
- DetailBadge.Text = evt.Success ? "성공" : "실패";
- DetailBadge.Background = new SolidColorBrush(evt.Success
- ? Color.FromRgb(0x34, 0xD3, 0x99)
- : Color.FromRgb(0xF8, 0x71, 0x71));
-
- var meta = $"시간: {evt.Timestamp:HH:mm:ss.fff}";
- if (evt.ElapsedMs > 0)
- meta += $" | 소요: {(evt.ElapsedMs >= 1000 ? $"{evt.ElapsedMs / 1000.0:F1}s" : $"{evt.ElapsedMs}ms")}";
- if (evt.InputTokens > 0 || evt.OutputTokens > 0)
- meta += $" | 토큰: 입력 {evt.InputTokens:#,0} / 출력 {evt.OutputTokens:#,0}";
- if (evt.Iteration > 0)
- meta += $" | 반복 #{evt.Iteration}";
- DetailMeta.Text = meta;
-
- var contentText = evt.Summary ?? "";
- if (!string.IsNullOrEmpty(evt.ToolInput))
- contentText += $"\n\n파라미터:\n{Truncate(evt.ToolInput, 500)}";
- if (!string.IsNullOrEmpty(evt.FilePath))
- contentText += $"\n파일: {evt.FilePath}";
- DetailContent.Text = contentText;
- }
-
- // ─── 요약 카드 업데이트 ──────────────────────────────────────
-
- private void UpdateSummaryCards()
- {
- var elapsed = (DateTime.Now - _startTime).TotalSeconds;
- CardElapsed.Text = elapsed >= 60 ? $"{elapsed / 60:F0}m {elapsed % 60:F0}s" : $"{elapsed:F1}s";
- CardIterations.Text = _maxIteration.ToString();
- var totalTokens = _totalInputTokens + _totalOutputTokens;
- CardTokens.Text = totalTokens >= 1000 ? $"{totalTokens / 1000.0:F1}k" : totalTokens.ToString();
- CardInputTokens.Text = _totalInputTokens >= 1000 ? $"{_totalInputTokens / 1000.0:F1}k" : _totalInputTokens.ToString();
- CardOutputTokens.Text = _totalOutputTokens >= 1000 ? $"{_totalOutputTokens / 1000.0:F1}k" : _totalOutputTokens.ToString();
- CardToolCalls.Text = _totalToolCalls.ToString();
- }
-
- // ─── 유틸리티 ────────────────────────────────────────────────
-
- private static string FormatMs(long ms)
- => ms >= 1000 ? $"{ms / 1000.0:F1}s" : $"{ms}ms";
-
- // ─── 윈도우 이벤트 ────────────────────────────────────────────
-
- private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- // 타이틀 바 버튼 영역 클릭 시 드래그 무시 (DragMove가 마우스를 캡처하여 MouseLeftButtonUp 차단 방지)
- var src = e.OriginalSource as DependencyObject;
- while (src != null && src != sender)
- {
- if (src is Border b && b.Name is "BtnClose" or "BtnMinimize" or "BtnClear")
- return;
- src = VisualTreeHelper.GetParent(src);
- }
- if (e.ClickCount == 1) DragMove();
- }
-
- private void BtnClose_Click(object sender, MouseButtonEventArgs e) => Hide();
- private void BtnMinimize_Click(object sender, MouseButtonEventArgs e) => WindowState = WindowState.Minimized;
- private void BtnClear_Click(object sender, MouseButtonEventArgs e) => Reset();
-
- private void TitleBtn_MouseEnter(object sender, MouseEventArgs e)
- {
- if (sender is Border b)
- b.Background = TryFindResource("ItemHoverBackground") as Brush
- ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
- }
-
- private void TitleBtn_MouseLeave(object sender, MouseEventArgs e)
- {
- if (sender is Border b)
- b.Background = Brushes.Transparent;
- }
-
- private static string Truncate(string text, int maxLen)
- => text.Length <= maxLen ? text : text[..maxLen] + "…";
-
- 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) & 0xFFFF)));
- const double grip = 8;
- var w = ActualWidth;
- var h = ActualHeight;
-
- if (pt.X < grip && pt.Y < grip) { handled = true; return (IntPtr)HTTOPLEFT; }
- if (pt.X > w - grip && pt.Y < grip) { handled = true; return (IntPtr)HTTOPRIGHT; }
- if (pt.X < grip && pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOMLEFT; }
- if (pt.X > w - grip && pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOMRIGHT; }
- if (pt.X < grip) { handled = true; return (IntPtr)HTLEFT; }
- if (pt.X > w - grip) { handled = true; return (IntPtr)HTRIGHT; }
- if (pt.Y < grip) { handled = true; return (IntPtr)HTTOP; }
- if (pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOM; }
- }
- return IntPtr.Zero;
- }
}
diff --git a/src/AxCopilot/Views/WorkflowAnalyzerWindow.Timeline.cs b/src/AxCopilot/Views/WorkflowAnalyzerWindow.Timeline.cs
new file mode 100644
index 0000000..51cdbbe
--- /dev/null
+++ b/src/AxCopilot/Views/WorkflowAnalyzerWindow.Timeline.cs
@@ -0,0 +1,281 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Shapes;
+using AxCopilot.Services.Agent;
+
+namespace AxCopilot.Views;
+
+public partial class WorkflowAnalyzerWindow
+{
+ // ─── 타임라인 노드 생성 ────────────────────────────────────────
+
+ private Border CreateTimelineNode(AgentEvent evt)
+ {
+ var (icon, iconColor, label) = GetEventVisual(evt);
+
+ var node = new Border
+ {
+ Background = Brushes.Transparent,
+ Margin = new Thickness(0, 1, 0, 1),
+ Padding = new Thickness(8, 6, 8, 6),
+ CornerRadius = new CornerRadius(8),
+ Cursor = Cursors.Hand,
+ Tag = evt,
+ };
+
+ var grid = new Grid();
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(28) });
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(3) });
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+
+ // 아이콘 원
+ var iconBorder = new Border
+ {
+ Width = 22, Height = 22,
+ CornerRadius = new CornerRadius(11),
+ Background = new SolidColorBrush(iconColor),
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Top,
+ Margin = new Thickness(0, 2, 0, 0),
+ };
+ iconBorder.Child = new TextBlock
+ {
+ Text = icon,
+ FontFamily = ThemeResourceHelper.SegoeMdl2,
+ FontSize = 10,
+ Foreground = Brushes.White,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ Grid.SetColumn(iconBorder, 0);
+ grid.Children.Add(iconBorder);
+
+ // 타임라인 세로 선
+ var line = new Rectangle
+ {
+ Width = 2,
+ Fill = TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Stretch,
+ Opacity = 0.4,
+ };
+ Grid.SetColumn(line, 1);
+ grid.Children.Add(line);
+
+ // 내용
+ var content = new StackPanel { Margin = new Thickness(8, 0, 0, 0) };
+
+ var headerPanel = new StackPanel { Orientation = Orientation.Horizontal };
+ headerPanel.Children.Add(new TextBlock
+ {
+ Text = label,
+ FontSize = 12,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
+ VerticalAlignment = VerticalAlignment.Center,
+ });
+
+ if (evt.ElapsedMs > 0)
+ {
+ headerPanel.Children.Add(new TextBlock
+ {
+ Text = evt.ElapsedMs >= 1000 ? $" {evt.ElapsedMs / 1000.0:F1}s" : $" {evt.ElapsedMs}ms",
+ FontSize = 10,
+ Foreground = evt.ElapsedMs > 3000
+ ? Brushes.OrangeRed
+ : TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
+ VerticalAlignment = VerticalAlignment.Center,
+ });
+ }
+
+ headerPanel.Children.Add(new TextBlock
+ {
+ Text = $" {evt.Timestamp:HH:mm:ss}",
+ FontSize = 9,
+ Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
+ VerticalAlignment = VerticalAlignment.Center,
+ Opacity = 0.6,
+ });
+ content.Children.Add(headerPanel);
+
+ if (!string.IsNullOrEmpty(evt.Summary))
+ {
+ content.Children.Add(new TextBlock
+ {
+ Text = Truncate(evt.Summary, 120),
+ FontSize = 11,
+ Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
+ TextWrapping = TextWrapping.Wrap,
+ Margin = new Thickness(0, 2, 0, 0),
+ LineHeight = 16,
+ });
+ }
+
+ if (evt.InputTokens > 0 || evt.OutputTokens > 0)
+ {
+ var tokenPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 0) };
+ tokenPanel.Children.Add(CreateBadge($"↑{evt.InputTokens:#,0}", "#3B82F6"));
+ tokenPanel.Children.Add(CreateBadge($"↓{evt.OutputTokens:#,0}", "#10B981"));
+ content.Children.Add(tokenPanel);
+ }
+
+ Grid.SetColumn(content, 2);
+ grid.Children.Add(content);
+ node.Child = grid;
+
+ var hoverBrush = TryFindResource("ItemHoverBackground") as Brush
+ ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
+ node.MouseEnter += (_, _) => node.Background = hoverBrush;
+ node.MouseLeave += (_, _) => node.Background = Brushes.Transparent;
+ node.MouseLeftButtonUp += (_, _) => ShowDetail(evt);
+
+ return node;
+ }
+
+ private static (string Icon, Color Color, string Label) GetEventVisual(AgentEvent evt)
+ {
+ return evt.Type switch
+ {
+ AgentEventType.Thinking => ("\uE7BA", Color.FromRgb(0x60, 0xA5, 0xFA), "사고"),
+ AgentEventType.Planning => ("\uE9D5", Color.FromRgb(0xA7, 0x8B, 0xFA), "계획"),
+ AgentEventType.StepStart => ("\uE72A", Color.FromRgb(0x60, 0xA5, 0xFA), $"단계 {evt.StepCurrent}/{evt.StepTotal}"),
+ AgentEventType.StepDone => ("\uE73E", Color.FromRgb(0x34, 0xD3, 0x99), $"단계 완료"),
+ AgentEventType.ToolCall => ("\uE756", Color.FromRgb(0xFB, 0xBF, 0x24), evt.ToolName),
+ AgentEventType.ToolResult=> ("\uE73E", Color.FromRgb(0x34, 0xD3, 0x99), $"{evt.ToolName} ✓"),
+ AgentEventType.SkillCall => ("\uE768", Color.FromRgb(0xA7, 0x8B, 0xFA), $"스킬: {evt.ToolName}"),
+ AgentEventType.Error => ("\uE783", Color.FromRgb(0xF8, 0x71, 0x71), $"{evt.ToolName} ✗"),
+ AgentEventType.Complete => ("\uE930", Color.FromRgb(0x34, 0xD3, 0x99), "완료"),
+ AgentEventType.Decision => ("\uE8C8", Color.FromRgb(0xFB, 0xBF, 0x24), "의사결정"),
+ _ => ("\uE946", Color.FromRgb(0x6B, 0x72, 0x80), "이벤트"),
+ };
+ }
+
+ private static Border CreateBadge(string text, string colorHex)
+ {
+ var color = (Color)ColorConverter.ConvertFromString(colorHex);
+ return new Border
+ {
+ Background = new SolidColorBrush(Color.FromArgb(0x30, color.R, color.G, color.B)),
+ CornerRadius = new CornerRadius(4),
+ Padding = new Thickness(5, 1, 5, 1),
+ Margin = new Thickness(0, 0, 4, 0),
+ Child = new TextBlock
+ {
+ Text = text,
+ FontSize = 9,
+ Foreground = new SolidColorBrush(color),
+ FontWeight = FontWeights.SemiBold,
+ },
+ };
+ }
+
+ // ─── 상세 패널 ─────────────────────────────────────────────
+
+ private void ShowDetail(AgentEvent evt)
+ {
+ DetailPanel.Visibility = Visibility.Visible;
+ DetailPanel.Tag = evt;
+ var (_, color, label) = GetEventVisual(evt);
+
+ DetailTitle.Text = label;
+ DetailBadge.Text = evt.Success ? "성공" : "실패";
+ DetailBadge.Background = new SolidColorBrush(evt.Success
+ ? Color.FromRgb(0x34, 0xD3, 0x99)
+ : Color.FromRgb(0xF8, 0x71, 0x71));
+
+ var meta = $"시간: {evt.Timestamp:HH:mm:ss.fff}";
+ if (evt.ElapsedMs > 0)
+ meta += $" | 소요: {(evt.ElapsedMs >= 1000 ? $"{evt.ElapsedMs / 1000.0:F1}s" : $"{evt.ElapsedMs}ms")}";
+ if (evt.InputTokens > 0 || evt.OutputTokens > 0)
+ meta += $" | 토큰: 입력 {evt.InputTokens:#,0} / 출력 {evt.OutputTokens:#,0}";
+ if (evt.Iteration > 0)
+ meta += $" | 반복 #{evt.Iteration}";
+ DetailMeta.Text = meta;
+
+ var contentText = evt.Summary ?? "";
+ if (!string.IsNullOrEmpty(evt.ToolInput))
+ contentText += $"\n\n파라미터:\n{Truncate(evt.ToolInput, 500)}";
+ if (!string.IsNullOrEmpty(evt.FilePath))
+ contentText += $"\n파일: {evt.FilePath}";
+ DetailContent.Text = contentText;
+ }
+
+ // ─── 요약 카드 업데이트 ──────────────────────────────────────
+
+ private void UpdateSummaryCards()
+ {
+ var elapsed = (DateTime.Now - _startTime).TotalSeconds;
+ CardElapsed.Text = elapsed >= 60 ? $"{elapsed / 60:F0}m {elapsed % 60:F0}s" : $"{elapsed:F1}s";
+ CardIterations.Text = _maxIteration.ToString();
+ var totalTokens = _totalInputTokens + _totalOutputTokens;
+ CardTokens.Text = totalTokens >= 1000 ? $"{totalTokens / 1000.0:F1}k" : totalTokens.ToString();
+ CardInputTokens.Text = _totalInputTokens >= 1000 ? $"{_totalInputTokens / 1000.0:F1}k" : _totalInputTokens.ToString();
+ CardOutputTokens.Text = _totalOutputTokens >= 1000 ? $"{_totalOutputTokens / 1000.0:F1}k" : _totalOutputTokens.ToString();
+ CardToolCalls.Text = _totalToolCalls.ToString();
+ }
+
+ // ─── 유틸리티 ────────────────────────────────────────────────
+
+ private static string FormatMs(long ms)
+ => ms >= 1000 ? $"{ms / 1000.0:F1}s" : $"{ms}ms";
+
+ private static string Truncate(string text, int maxLen)
+ => text.Length <= maxLen ? text : text[..maxLen] + "…";
+
+ // ─── 윈도우 이벤트 ────────────────────────────────────────────
+
+ private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ // 타이틀 바 버튼 영역 클릭 시 드래그 무시 (DragMove가 마우스를 캡처하여 MouseLeftButtonUp 차단 방지)
+ var src = e.OriginalSource as DependencyObject;
+ while (src != null && src != sender)
+ {
+ if (src is Border b && b.Name is "BtnClose" or "BtnMinimize" or "BtnClear")
+ return;
+ src = VisualTreeHelper.GetParent(src);
+ }
+ if (e.ClickCount == 1) DragMove();
+ }
+
+ private void BtnClose_Click(object sender, MouseButtonEventArgs e) => Hide();
+ private void BtnMinimize_Click(object sender, MouseButtonEventArgs e) => WindowState = WindowState.Minimized;
+ private void BtnClear_Click(object sender, MouseButtonEventArgs e) => Reset();
+
+ private void TitleBtn_MouseEnter(object sender, MouseEventArgs e)
+ {
+ if (sender is Border b)
+ b.Background = TryFindResource("ItemHoverBackground") as Brush
+ ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
+ }
+
+ private void TitleBtn_MouseLeave(object sender, MouseEventArgs e)
+ {
+ if (sender is Border b)
+ b.Background = Brushes.Transparent;
+ }
+
+ 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) & 0xFFFF)));
+ const double grip = 8;
+ var w = ActualWidth;
+ var h = ActualHeight;
+
+ if (pt.X < grip && pt.Y < grip) { handled = true; return (IntPtr)HTTOPLEFT; }
+ if (pt.X > w - grip && pt.Y < grip) { handled = true; return (IntPtr)HTTOPRIGHT; }
+ if (pt.X < grip && pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOMLEFT; }
+ if (pt.X > w - grip && pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOMRIGHT; }
+ if (pt.X < grip) { handled = true; return (IntPtr)HTLEFT; }
+ if (pt.X > w - grip) { handled = true; return (IntPtr)HTRIGHT; }
+ if (pt.Y < grip) { handled = true; return (IntPtr)HTTOP; }
+ if (pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOM; }
+ }
+ return IntPtr.Zero;
+ }
+}