AX Agent transcript 메시지 버블 렌더를 분리해 메인 창 구조를 정리한다
Some checks failed
Release Gate / gate (push) Has been cancelled
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user