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:
@@ -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의 책임 경계가 더 분명해졌다.
|
- 프리셋 카드와 주제 선택 흐름을 [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)
|
- 업데이트: 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에서 관리되게 정리했다.
|
- 좌측 대화 목록 렌더를 [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에서 관리되게 정리했다.
|
||||||
|
|||||||
@@ -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: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) - 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: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.
|
||||||
|
|||||||
330
src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs
Normal file
330
src/AxCopilot/Views/ChatWindow.MessageBubblePresentation.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2970,337 +2970,6 @@ public partial class ChatWindow : Window
|
|||||||
return wrapper;
|
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>
|
/// <summary>커스텀 체크/미선택 아이콘을 생성합니다. Path 도형 기반, 선택 시 스케일 바운스 애니메이션.</summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user