Files
AX-Copilot-Codex/src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs
lacvet 7c5396e239
Some checks failed
Release Gate / gate (push) Has been cancelled
AX Agent 사용자 메시지 메타 겹침 수정
사용자 메시지 하단 메타 행에서 시간 표시와 복사/편집 액션이 같은 위치에 겹치던 레이아웃을 수정했습니다.

ChatWindow.MessageBubblePresentation에서 사용자 메타 바를 전용 컬럼 구조로 분리해 시간과 액션 버튼이 항상 분리되어 표시되도록 정리했습니다.

README와 docs/DEVELOPMENT.md에 수정 이력을 2026-04-06 15:04 (KST) 기준으로 반영했습니다.

검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-06 15:07:18 +09:00

338 lines
14 KiB
C#

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) };
userBottomBar.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
userBottomBar.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
userBottomBar.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var timestamp = message?.Timestamp ?? DateTime.Now;
Grid.SetColumn(userActionBar, 1);
userBottomBar.Children.Add(userActionBar);
var timestampText = 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(8, 0, 2, 1),
};
Grid.SetColumn(timestampText, 2);
userBottomBar.Children.Add(timestampText);
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);
}
}