AX Agent transcript 메시지 버블 렌더를 분리해 메인 창 구조를 정리한다
Some checks failed
Release Gate / gate (push) Has been cancelled

사용자/assistant 메시지 버블, 분기 컨텍스트 카드, 액션 바, 응답 메타 조립을 ChatWindow.MessageBubblePresentation.cs로 이동해 ChatWindow.xaml.cs가 transcript orchestration과 런타임 흐름에 더 집중하도록 정리했다.

README와 DEVELOPMENT 문서에 2026-04-06 10:27 (KST) 기준 이력을 반영했고, dotnet build 검증 결과 경고 0 / 오류 0을 확인했다.
This commit is contained in:
2026-04-06 10:29:54 +09:00
parent 8faa26b134
commit b3b5f8a79d
4 changed files with 334 additions and 331 deletions

View File

@@ -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에서 관리되게 정리했다.

View File

@@ -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.

View File

@@ -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);
}
}

View File

@@ -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);
}
}
// ─── 커스텀 체크 아이콘 (모든 팝업 메뉴 공통) ─────────────────────────
/// <summary>커스텀 체크/미선택 아이콘을 생성합니다. Path 도형 기반, 선택 시 스케일 바운스 애니메이션.</summary>