diff --git a/README.md b/README.md index 63c27a0..d3421d9 100644 --- a/README.md +++ b/README.md @@ -1182,3 +1182,5 @@ MIT License - 프리셋 카드와 주제 선택 흐름을 [ChatWindow.TopicPresetPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TopicPresetPresentation.cs) 로 분리했다. `BuildTopicButtons`, `ShowCustomPresetDialog`, `ShowCustomPresetContextMenu`, `SelectTopic`이 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 밖으로 이동해, 프리셋 UI와 대화 orchestration의 책임 경계가 더 분명해졌다. - 업데이트: 2026-04-06 10:18 (KST) - 좌측 대화 목록 렌더를 [ChatWindow.ConversationListPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs) 로 분리했다. `RefreshConversationList`, `RenderConversationList`, `AddLoadMoreButton`, `BuildConversationSpotlightItems`, `AddGroupHeader`, `AddConversationItem`이 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 밖으로 이동해, 메인 창은 transcript/runtime orchestration에 더 집중하고 목록 UI는 별도 presentation surface에서 관리되게 정리했다. +- 업데이트: 2026-04-06 10:27 (KST) + - transcript 메시지 row 조립을 [ChatWindow.MessageBubblePresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs) 로 분리했다. `AddMessageBubble(...)`가 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 밖으로 이동해, 사용자/assistant bubble, 분기 컨텍스트 카드, 액션 바와 메타 row 조립이 별도 presentation surface에서 관리되게 정리했다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index a0f181c..326df8c 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4923,3 +4923,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - Document update: 2026-04-06 10:07 (KST) - This keeps the main chat window more orchestration-focused and narrows the remaining maintainability work to small follow-up polish rather than any large structural split. - Document update: 2026-04-06 10:18 (KST) - Split conversation list rendering out of `ChatWindow.xaml.cs` into `ChatWindow.ConversationListPresentation.cs`. Conversation meta filtering, spotlight calculation, grouped list rendering, load-more pagination, and sidebar conversation-row assembly now live in a dedicated partial. - Document update: 2026-04-06 10:18 (KST) - This keeps the main chat window more focused on transcript/runtime orchestration while isolating sidebar list presentation for future UX tuning and parity work. +- Document update: 2026-04-06 10:27 (KST) - Split transcript message-row assembly out of `ChatWindow.xaml.cs` into `ChatWindow.MessageBubblePresentation.cs`. The shared `AddMessageBubble(...)` path for user/assistant bubbles, branch-context cards, inline action bars, and assistant meta rows now lives in a dedicated presentation partial. +- Document update: 2026-04-06 10:27 (KST) - This keeps the main chat window more orchestration-focused while making transcript row rendering easier to tune and extend independently. diff --git a/src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs b/src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs new file mode 100644 index 0000000..f3be835 --- /dev/null +++ b/src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs @@ -0,0 +1,330 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using AxCopilot.Models; +using AxCopilot.Services; + +namespace AxCopilot.Views; + +public partial class ChatWindow +{ + private void AddMessageBubble(string role, string content, bool animate = true, ChatMessage? message = null) + { + var isUser = role == "user"; + var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; + var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; + var itemBg = TryFindResource("ItemBackground") as Brush + ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40)); + var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; + var hintBg = TryFindResource("HintBackground") as Brush + ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); + var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; + var userBubbleBg = hintBg; + var assistantBubbleBg = itemBg; + + if (isUser) + { + var msgMaxWidth = GetMessageMaxWidth(); + var wrapper = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + Width = msgMaxWidth, + MaxWidth = msgMaxWidth, + Margin = new Thickness(0, 4, 0, 6), + }; + + var bubble = new Border + { + Background = userBubbleBg, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(9), + Padding = new Thickness(11, 7, 11, 7), + HorizontalAlignment = HorizontalAlignment.Right, + }; + + if (string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase) || + string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase)) + { + var userCodeBgBrush = TryFindResource("HintBackground") as Brush ?? Brushes.DarkGray; + MarkdownRenderer.EnableFilePathHighlight = + (System.Windows.Application.Current as App)?.SettingsService?.Settings.Llm.EnableFilePathHighlight ?? true; + bubble.Child = MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, userCodeBgBrush); + } + else + { + bubble.Child = new TextBlock + { + Text = content, + TextAlignment = TextAlignment.Left, + FontSize = 12, + Foreground = primaryText, + TextWrapping = TextWrapping.Wrap, + LineHeight = 18, + }; + } + + wrapper.Children.Add(bubble); + + var userActionBar = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Right, + Opacity = 0.8, + Margin = new Thickness(0, 2, 0, 0), + }; + var capturedUserContent = content; + var userBtnColor = secondaryText; + userActionBar.Children.Add(CreateActionButton("\uE8C8", "복사", userBtnColor, () => + { + try { Clipboard.SetText(capturedUserContent); } catch { } + })); + userActionBar.Children.Add(CreateActionButton("\uE70F", "편집", userBtnColor, + () => EnterEditMode(wrapper, capturedUserContent))); + + var userBottomBar = new Grid { Margin = new Thickness(0, 1, 0, 0) }; + var timestamp = message?.Timestamp ?? DateTime.Now; + userBottomBar.Children.Add(new TextBlock + { + Text = timestamp.ToString("HH:mm"), + FontSize = 10.5, + Opacity = 0.52, + Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, + HorizontalAlignment = HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 2, 1), + }); + userBottomBar.Children.Add(userActionBar); + wrapper.Children.Add(userBottomBar); + wrapper.MouseEnter += (_, _) => userActionBar.Opacity = 1; + wrapper.MouseLeave += (_, _) => userActionBar.Opacity = ReferenceEquals(_selectedMessageActionBar, userActionBar) ? 1 : 0.8; + wrapper.MouseLeftButtonUp += (_, _) => SelectMessageActionBar(userActionBar, bubble); + + var userContent = content; + wrapper.MouseRightButtonUp += (_, re) => + { + re.Handled = true; + ShowMessageContextMenu(userContent, "user"); + }; + + if (animate) + ApplyMessageEntryAnimation(wrapper); + MessagePanel.Children.Add(wrapper); + return; + } + + if (message != null && IsCompactionMetaMessage(message)) + { + var compactCard = CreateCompactionMetaCard(message, primaryText, secondaryText, hintBg, borderBrush, accentBrush); + if (animate) + ApplyMessageEntryAnimation(compactCard); + MessagePanel.Children.Add(compactCard); + return; + } + + var assistantMaxWidth = GetMessageMaxWidth(); + var container = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + Width = assistantMaxWidth, + MaxWidth = assistantMaxWidth, + Margin = new Thickness(0, 6, 0, 6), + }; + if (animate) + ApplyMessageEntryAnimation(container); + + var (agentName, _, _) = GetAgentIdentity(); + var header = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(2, 0, 0, 1.5) }; + header.Children.Add(new TextBlock + { + Text = "\uE945", + FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontSize = 10, + Foreground = secondaryText, + VerticalAlignment = VerticalAlignment.Center, + }); + header.Children.Add(new TextBlock + { + Text = agentName, + FontSize = 11.5, + FontWeight = FontWeights.SemiBold, + Foreground = secondaryText, + Margin = new Thickness(4, 0, 0, 0), + VerticalAlignment = VerticalAlignment.Center, + }); + container.Children.Add(header); + + var contentCard = new Border + { + Background = assistantBubbleBg, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(9), + Padding = new Thickness(11, 8, 11, 8), + }; + var contentStack = new StackPanel(); + + var app = System.Windows.Application.Current as App; + MarkdownRenderer.EnableFilePathHighlight = + app?.SettingsService?.Settings.Llm.EnableFilePathHighlight ?? true; + var codeBgBrush = TryFindResource("HintBackground") as Brush ?? Brushes.DarkGray; + if (IsBranchContextMessage(content)) + { + var branchRun = GetAgentRunStateById(message?.MetaRunId) ?? GetLatestBranchContextRun(); + var branchFiles = GetBranchContextFilePaths(message?.MetaRunId ?? branchRun?.RunId, 3); + var branchCard = new Border + { + Background = hintBg, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(12), + Padding = new Thickness(12, 10, 12, 10), + Margin = new Thickness(0, 0, 0, 6), + }; + var branchStack = new StackPanel(); + branchStack.Children.Add(new TextBlock + { + Text = "분기 컨텍스트", + FontSize = 11, + FontWeight = FontWeights.SemiBold, + Foreground = primaryText, + Margin = new Thickness(0, 0, 0, 6), + }); + branchStack.Children.Add(MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, codeBgBrush)); + + if (branchFiles.Count > 0) + { + var filesWrap = new WrapPanel { Margin = new Thickness(0, 8, 0, 0) }; + foreach (var path in branchFiles) + { + var fileButton = new Button + { + Background = itemBg, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1), + Padding = new Thickness(8, 3, 8, 3), + Margin = new Thickness(0, 0, 6, 6), + Cursor = Cursors.Hand, + ToolTip = path, + Content = new TextBlock + { + Text = System.IO.Path.GetFileName(path), + FontSize = 10, + FontWeight = FontWeights.SemiBold, + Foreground = primaryText, + }, + }; + var capturedPath = path; + fileButton.Click += (_, _) => OpenRunFilePath(capturedPath); + filesWrap.Children.Add(fileButton); + } + branchStack.Children.Add(filesWrap); + } + + if (branchRun != null) + { + var actionsWrap = new WrapPanel { Margin = new Thickness(0, 8, 0, 0) }; + + var followUpButton = new Button + { + Background = itemBg, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1), + Padding = new Thickness(8, 3, 8, 3), + Margin = new Thickness(0, 0, 6, 6), + Cursor = Cursors.Hand, + Content = new TextBlock + { + Text = "후속 작업 큐에 넣기", + FontSize = 10, + FontWeight = FontWeights.SemiBold, + Foreground = primaryText, + }, + }; + var capturedBranchRun = branchRun; + followUpButton.Click += (_, _) => EnqueueFollowUpFromRun(capturedBranchRun); + actionsWrap.Children.Add(followUpButton); + + var timelineButton = new Button + { + Background = itemBg, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1), + Padding = new Thickness(8, 3, 8, 3), + Margin = new Thickness(0, 0, 6, 6), + Cursor = Cursors.Hand, + Content = new TextBlock + { + Text = "관련 로그로 이동", + FontSize = 10, + FontWeight = FontWeights.SemiBold, + Foreground = primaryText, + }, + }; + timelineButton.Click += (_, _) => ScrollToRunInTimeline(capturedBranchRun.RunId); + actionsWrap.Children.Add(timelineButton); + + branchStack.Children.Add(actionsWrap); + } + + branchCard.Child = branchStack; + contentStack.Children.Add(branchCard); + } + else + { + contentStack.Children.Add(MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, codeBgBrush)); + } + + contentCard.Child = contentStack; + container.Children.Add(contentCard); + + var actionBar = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Left, + Margin = new Thickness(2, 2, 0, 0), + Opacity = 0.8, + }; + var btnColor = secondaryText; + var capturedContent = content; + + actionBar.Children.Add(CreateActionButton("\uE8C8", "복사", btnColor, () => + { + try { Clipboard.SetText(capturedContent); } catch { } + })); + actionBar.Children.Add(CreateActionButton("\uE72C", "다시 생성", btnColor, () => _ = RegenerateLastAsync())); + actionBar.Children.Add(CreateActionButton("\uE70F", "수정 후 재시도", btnColor, () => ShowRetryWithFeedbackInput())); + AddLinkedFeedbackButtons(actionBar, btnColor, message); + + var aiTimestamp = message?.Timestamp ?? DateTime.Now; + actionBar.Children.Add(new TextBlock + { + Text = aiTimestamp.ToString("HH:mm"), + FontSize = 10.5, + Opacity = 0.52, + Foreground = btnColor, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(4, 0, 0, 1), + }); + + container.Children.Add(actionBar); + var assistantMeta = CreateAssistantMessageMetaText(message); + if (assistantMeta != null) + container.Children.Add(assistantMeta); + + container.MouseEnter += (_, _) => actionBar.Opacity = 1; + container.MouseLeave += (_, _) => actionBar.Opacity = ReferenceEquals(_selectedMessageActionBar, actionBar) ? 1 : 0.8; + container.MouseLeftButtonUp += (_, _) => SelectMessageActionBar(actionBar, contentCard); + + var aiContent = content; + container.MouseRightButtonUp += (_, re) => + { + re.Handled = true; + ShowMessageContextMenu(aiContent, "assistant"); + }; + + MessagePanel.Children.Add(container); + } +} diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 77cf239..e6b828b 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -2970,337 +2970,6 @@ public partial class ChatWindow : Window return wrapper; } - private void AddMessageBubble(string role, string content, bool animate = true, ChatMessage? message = null) - { - var isUser = role == "user"; - var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; - var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; - var itemBg = TryFindResource("ItemBackground") as Brush - ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40)); - var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; - var hintBg = TryFindResource("HintBackground") as Brush - ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); - var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; - var userBubbleBg = hintBg; - var assistantBubbleBg = itemBg; - - if (isUser) - { - // 사용자: claw-code 쪽처럼 더 얇은 transcript 버블 - var msgMaxWidth = GetMessageMaxWidth(); - var wrapper = new StackPanel - { - HorizontalAlignment = HorizontalAlignment.Center, - Width = msgMaxWidth, - MaxWidth = msgMaxWidth, - Margin = new Thickness(0, 4, 0, 6), - }; - - var bubble = new Border - { - Background = userBubbleBg, - BorderBrush = borderBrush, - BorderThickness = new Thickness(1), - CornerRadius = new CornerRadius(9), - Padding = new Thickness(11, 7, 11, 7), - HorizontalAlignment = HorizontalAlignment.Right, - }; - if (string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase) || - string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase)) - { - var codeBgBrush = TryFindResource("HintBackground") as Brush ?? Brushes.DarkGray; - MarkdownRenderer.EnableFilePathHighlight = - (System.Windows.Application.Current as App)?.SettingsService?.Settings.Llm.EnableFilePathHighlight ?? true; - bubble.Child = MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, codeBgBrush); - } - else - { - bubble.Child = new TextBlock - { - Text = content, - TextAlignment = TextAlignment.Left, - FontSize = 12, - Foreground = primaryText, - TextWrapping = TextWrapping.Wrap, - LineHeight = 18, - }; - } - wrapper.Children.Add(bubble); - - // 액션 버튼 바 (복사 + 편집, hover 시 표시) - var userActionBar = new StackPanel - { - Orientation = Orientation.Horizontal, - HorizontalAlignment = HorizontalAlignment.Right, - Opacity = 0.8, - Margin = new Thickness(0, 2, 0, 0), - }; - var capturedUserContent = content; - var userBtnColor = secondaryText; - userActionBar.Children.Add(CreateActionButton("\uE8C8", "복사", userBtnColor, () => - { - try { Clipboard.SetText(capturedUserContent); } catch { } - })); - userActionBar.Children.Add(CreateActionButton("\uE70F", "편집", userBtnColor, - () => EnterEditMode(wrapper, capturedUserContent))); - - // 타임스탬프 + 액션 바 - var userBottomBar = new Grid { Margin = new Thickness(0, 1, 0, 0) }; - var timestamp = message?.Timestamp ?? DateTime.Now; - userBottomBar.Children.Add(new TextBlock - { - Text = timestamp.ToString("HH:mm"), - FontSize = 10.5, Opacity = 0.52, - Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, - HorizontalAlignment = HorizontalAlignment.Right, - VerticalAlignment = VerticalAlignment.Center, - Margin = new Thickness(0, 0, 2, 1), - }); - userBottomBar.Children.Add(userActionBar); - wrapper.Children.Add(userBottomBar); - wrapper.MouseEnter += (_, _) => userActionBar.Opacity = 1; - wrapper.MouseLeave += (_, _) => userActionBar.Opacity = ReferenceEquals(_selectedMessageActionBar, userActionBar) ? 1 : 0.8; - wrapper.MouseLeftButtonUp += (_, _) => SelectMessageActionBar(userActionBar, bubble); - - // 우클릭 → 메시지 컨텍스트 메뉴 - var userContent = content; - wrapper.MouseRightButtonUp += (_, re) => - { - re.Handled = true; - ShowMessageContextMenu(userContent, "user"); - }; - - if (animate) ApplyMessageEntryAnimation(wrapper); - MessagePanel.Children.Add(wrapper); - } - else - { - if (message != null && IsCompactionMetaMessage(message)) - { - var compactCard = CreateCompactionMetaCard(message, primaryText, secondaryText, hintBg, borderBrush, accentBrush); - if (animate) ApplyMessageEntryAnimation(compactCard); - MessagePanel.Children.Add(compactCard); - return; - } - - // 어시스턴트: 카드보다 transcript 행에 가까운 스타일 - var msgMaxWidth = GetMessageMaxWidth(); - var container = new StackPanel - { - HorizontalAlignment = HorizontalAlignment.Center, - Width = msgMaxWidth, - MaxWidth = msgMaxWidth, - Margin = new Thickness(0, 6, 0, 6) - }; - if (animate) ApplyMessageEntryAnimation(container); - - // AI 에이전트 이름 + 아이콘 - var (agentName, _, _) = GetAgentIdentity(); - var headerSp = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(2, 0, 0, 1.5) }; - - var iconBlock = new TextBlock - { - Text = "\uE945", - FontFamily = new FontFamily("Segoe MDL2 Assets"), - FontSize = 10, - Foreground = secondaryText, - VerticalAlignment = VerticalAlignment.Center, - }; - headerSp.Children.Add(iconBlock); - - headerSp.Children.Add(new TextBlock - { - Text = agentName, - FontSize = 11.5, - FontWeight = FontWeights.SemiBold, - Foreground = secondaryText, - Margin = new Thickness(4, 0, 0, 0), - VerticalAlignment = VerticalAlignment.Center, - }); - container.Children.Add(headerSp); - - var contentCard = new Border - { - Background = assistantBubbleBg, - BorderBrush = borderBrush, - BorderThickness = new Thickness(1), - CornerRadius = new CornerRadius(9), - Padding = new Thickness(11, 8, 11, 8), - }; - var contentStack = new StackPanel(); - - // 마크다운 렌더링 (파일 경로 강조 설정 연동) - var app = System.Windows.Application.Current as App; - MarkdownRenderer.EnableFilePathHighlight = - app?.SettingsService?.Settings.Llm.EnableFilePathHighlight ?? true; - var codeBgBrush = TryFindResource("HintBackground") as Brush ?? Brushes.DarkGray; - if (IsBranchContextMessage(content)) - { - var branchRun = GetAgentRunStateById(message?.MetaRunId) ?? GetLatestBranchContextRun(); - var branchFiles = GetBranchContextFilePaths(message?.MetaRunId ?? branchRun?.RunId, 3); - var branchCard = new Border - { - Background = hintBg, - BorderBrush = borderBrush, - BorderThickness = new Thickness(1), - CornerRadius = new CornerRadius(12), - Padding = new Thickness(12, 10, 12, 10), - Margin = new Thickness(0, 0, 0, 6), - }; - var branchStack = new StackPanel(); - branchStack.Children.Add(new TextBlock - { - Text = "분기 컨텍스트", - FontSize = 11, - FontWeight = FontWeights.SemiBold, - Foreground = primaryText, - Margin = new Thickness(0, 0, 0, 6), - }); - var branchMd = MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, codeBgBrush); - branchStack.Children.Add(branchMd); - if (branchFiles.Count > 0) - { - var filesWrap = new WrapPanel - { - Margin = new Thickness(0, 8, 0, 0), - }; - foreach (var path in branchFiles) - { - var fileButton = new Button - { - Background = itemBg, - BorderBrush = borderBrush, - BorderThickness = new Thickness(1), - Padding = new Thickness(8, 3, 8, 3), - Margin = new Thickness(0, 0, 6, 6), - Cursor = Cursors.Hand, - ToolTip = path, - Content = new TextBlock - { - Text = System.IO.Path.GetFileName(path), - FontSize = 10, - FontWeight = FontWeights.SemiBold, - Foreground = primaryText, - } - }; - var capturedPath = path; - fileButton.Click += (_, _) => OpenRunFilePath(capturedPath); - filesWrap.Children.Add(fileButton); - } - branchStack.Children.Add(filesWrap); - } - - if (branchRun != null) - { - var actionsWrap = new WrapPanel - { - Margin = new Thickness(0, 8, 0, 0), - }; - - var followUpButton = new Button - { - Background = itemBg, - BorderBrush = borderBrush, - BorderThickness = new Thickness(1), - Padding = new Thickness(8, 3, 8, 3), - Margin = new Thickness(0, 0, 6, 6), - Cursor = Cursors.Hand, - Content = new TextBlock - { - Text = "후속 작업 큐에 넣기", - FontSize = 10, - FontWeight = FontWeights.SemiBold, - Foreground = primaryText, - } - }; - var capturedBranchRun = branchRun; - followUpButton.Click += (_, _) => EnqueueFollowUpFromRun(capturedBranchRun); - actionsWrap.Children.Add(followUpButton); - - var timelineButton = new Button - { - Background = itemBg, - BorderBrush = borderBrush, - BorderThickness = new Thickness(1), - Padding = new Thickness(8, 3, 8, 3), - Margin = new Thickness(0, 0, 6, 6), - Cursor = Cursors.Hand, - Content = new TextBlock - { - Text = "관련 로그로 이동", - FontSize = 10, - FontWeight = FontWeights.SemiBold, - Foreground = primaryText, - } - }; - timelineButton.Click += (_, _) => ScrollToRunInTimeline(capturedBranchRun.RunId); - actionsWrap.Children.Add(timelineButton); - - branchStack.Children.Add(actionsWrap); - } - branchCard.Child = branchStack; - contentStack.Children.Add(branchCard); - } - else - { - var mdPanel = MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, codeBgBrush); - contentStack.Children.Add(mdPanel); - } - contentCard.Child = contentStack; - container.Children.Add(contentCard); - - // 액션 버튼 바 (복사 / 좋아요 / 싫어요) - var actionBar = new StackPanel - { - Orientation = Orientation.Horizontal, - HorizontalAlignment = HorizontalAlignment.Left, - Margin = new Thickness(2, 2, 0, 0), - Opacity = 0.8 - }; - - var btnColor = secondaryText; - var capturedContent = content; - - actionBar.Children.Add(CreateActionButton("\uE8C8", "복사", btnColor, () => - { - try { Clipboard.SetText(capturedContent); } catch { } - })); - actionBar.Children.Add(CreateActionButton("\uE72C", "다시 생성", btnColor, () => _ = RegenerateLastAsync())); - actionBar.Children.Add(CreateActionButton("\uE70F", "수정 후 재시도", btnColor, () => ShowRetryWithFeedbackInput())); - AddLinkedFeedbackButtons(actionBar, btnColor, message); - - // 타임스탬프 - var aiTimestamp = message?.Timestamp ?? DateTime.Now; - actionBar.Children.Add(new TextBlock - { - Text = aiTimestamp.ToString("HH:mm"), - FontSize = 10.5, Opacity = 0.52, - Foreground = btnColor, - VerticalAlignment = VerticalAlignment.Center, - Margin = new Thickness(4, 0, 0, 1), - }); - - container.Children.Add(actionBar); - var assistantMeta = CreateAssistantMessageMetaText(message); - if (assistantMeta != null) - container.Children.Add(assistantMeta); - - container.MouseEnter += (_, _) => actionBar.Opacity = 1; - container.MouseLeave += (_, _) => actionBar.Opacity = ReferenceEquals(_selectedMessageActionBar, actionBar) ? 1 : 0.8; - container.MouseLeftButtonUp += (_, _) => SelectMessageActionBar(actionBar, contentCard); - - // 우클릭 → 메시지 컨텍스트 메뉴 - var aiContent = content; - container.MouseRightButtonUp += (_, re) => - { - re.Handled = true; - ShowMessageContextMenu(aiContent, "assistant"); - }; - - MessagePanel.Children.Add(container); - } - } - // ─── 커스텀 체크 아이콘 (모든 팝업 메뉴 공통) ───────────────────────── /// 커스텀 체크/미선택 아이콘을 생성합니다. Path 도형 기반, 선택 시 스케일 바운스 애니메이션.