using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using AxCopilot.Models; using AxCopilot.Services.Agent; namespace AxCopilot.Views; public partial class ChatWindow { private List GetVisibleTimelineMessages(ChatConversation? conversation) { return conversation?.Messages?.Where(msg => { if (string.Equals(msg.Role, "system", StringComparison.OrdinalIgnoreCase)) return false; if (string.Equals(msg.Role, "assistant", StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace(msg.Content)) return false; return true; }).ToList() ?? new List(); } private List GetVisibleTimelineEvents(ChatConversation? conversation) { var events = conversation?.ExecutionEvents?.ToList() ?? new List(); if (conversation?.ShowExecutionHistory ?? true) return events; return events .Where(ShouldShowCollapsedProgressEvent) .ToList(); } private static bool ShouldShowCollapsedProgressEvent(ChatExecutionEvent executionEvent) { var restoredEvent = ToAgentEvent(executionEvent); if (restoredEvent.Type == AgentEventType.Complete || restoredEvent.Type == AgentEventType.Error) return true; if (restoredEvent.Type == AgentEventType.Thinking) { if (string.Equals(restoredEvent.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase) || string.Equals(restoredEvent.ToolName, "context_compaction", StringComparison.OrdinalIgnoreCase)) return true; if (!string.IsNullOrWhiteSpace(restoredEvent.Summary)) return true; } return IsProcessFeedEvent(restoredEvent); } private List<(DateTime Timestamp, int Order, Action Render)> BuildTimelineRenderActions( IReadOnlyCollection visibleMessages, IReadOnlyCollection visibleEvents) { var timeline = new List<(DateTime Timestamp, int Order, Action Render)>(visibleMessages.Count + visibleEvents.Count); foreach (var msg in visibleMessages) timeline.Add((msg.Timestamp, 0, () => AddMessageBubble(msg.Role, msg.Content, animate: false, message: msg))); foreach (var executionEvent in visibleEvents) { var restoredEvent = ToAgentEvent(executionEvent); timeline.Add((executionEvent.Timestamp, 1, () => AddAgentEventBanner(restoredEvent))); } var liveProgressHint = GetLiveAgentProgressHint(); if (liveProgressHint != null) timeline.Add((liveProgressHint.Timestamp, 2, () => AddAgentEventBanner(liveProgressHint))); return timeline .OrderBy(x => x.Timestamp) .ThenBy(x => x.Order) .ToList(); } private Border CreateTimelineLoadMoreCard(int hiddenCount) { var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? BrushFromHex("#F8FAFC"); var borderBrush = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E2E8F0"); var primaryText = TryFindResource("PrimaryText") as Brush ?? BrushFromHex("#334155"); var secondaryText = TryFindResource("SecondaryText") as Brush ?? BrushFromHex("#64748B"); var loadMoreBtn = new Button { Background = Brushes.Transparent, BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(7, 3, 7, 3), Cursor = System.Windows.Input.Cursors.Hand, Foreground = primaryText, HorizontalAlignment = HorizontalAlignment.Center, }; loadMoreBtn.Template = BuildMinimalIconButtonTemplate(); loadMoreBtn.Content = new StackPanel { Orientation = Orientation.Horizontal, Children = { new TextBlock { Text = "\uE70D", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 8, Foreground = secondaryText, Margin = new Thickness(0, 0, 4, 0), VerticalAlignment = VerticalAlignment.Center, }, new TextBlock { Text = $"이전 대화 {hiddenCount:N0}개", FontSize = 9.25, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, } } }; loadMoreBtn.MouseEnter += (_, _) => loadMoreBtn.Background = hoverBg; loadMoreBtn.MouseLeave += (_, _) => loadMoreBtn.Background = Brushes.Transparent; loadMoreBtn.Click += (_, _) => { _timelineRenderLimit += TimelineRenderPageSize; RenderMessages(preserveViewport: true); }; return new Border { CornerRadius = new CornerRadius(10), Margin = new Thickness(0, 2, 0, 8), Padding = new Thickness(0), Background = Brushes.Transparent, BorderBrush = Brushes.Transparent, BorderThickness = new Thickness(0), HorizontalAlignment = HorizontalAlignment.Center, Child = loadMoreBtn, }; } private static AgentEvent ToAgentEvent(ChatExecutionEvent executionEvent) { var parsedType = Enum.TryParse(executionEvent.Type, out var eventType) ? eventType : AgentEventType.Thinking; return new AgentEvent { Timestamp = executionEvent.Timestamp, RunId = executionEvent.RunId, Type = parsedType, ToolName = executionEvent.ToolName, Summary = executionEvent.Summary, FilePath = executionEvent.FilePath, Success = executionEvent.Success, StepCurrent = executionEvent.StepCurrent, StepTotal = executionEvent.StepTotal, Steps = executionEvent.Steps, ElapsedMs = executionEvent.ElapsedMs, InputTokens = executionEvent.InputTokens, OutputTokens = executionEvent.OutputTokens, }; } private static bool IsCompactionMetaMessage(ChatMessage? message) { var kind = message?.MetaKind ?? ""; return kind.Equals("microcompact_boundary", StringComparison.OrdinalIgnoreCase) || kind.Equals("session_memory_compaction", StringComparison.OrdinalIgnoreCase) || kind.Equals("collapsed_boundary", StringComparison.OrdinalIgnoreCase); } private Border CreateCompactionMetaCard(ChatMessage message, Brush primaryText, Brush secondaryText, Brush hintBg, Brush borderBrush, Brush accentBrush) { var icon = "\uE9CE"; var title = message.MetaKind switch { "session_memory_compaction" => "세션 메모리 압축", "collapsed_boundary" => "압축 경계 병합", _ => "Microcompact 경계", }; var wrapper = new Border { Background = hintBg, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(12), Padding = new Thickness(12, 10, 12, 10), Margin = new Thickness(10, 4, 150, 4), MaxWidth = GetMessageMaxWidth(), HorizontalAlignment = HorizontalAlignment.Left, }; var stack = new StackPanel(); var header = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 6), }; header.Children.Add(new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, }); header.Children.Add(new TextBlock { Text = title, FontSize = 11, FontWeight = FontWeights.SemiBold, Foreground = primaryText, Margin = new Thickness(6, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center, }); stack.Children.Add(header); var lines = (message.Content ?? "") .Replace("\r\n", "\n") .Split('\n', StringSplitOptions.RemoveEmptyEntries) .Select(line => line.Trim()) .Where(line => !string.IsNullOrWhiteSpace(line)) .ToList(); foreach (var line in lines) { var isHeaderLine = line.StartsWith("[", StringComparison.Ordinal); stack.Children.Add(new TextBlock { Text = isHeaderLine ? line.Trim('[', ']') : line, FontSize = 10.5, FontWeight = isHeaderLine ? FontWeights.SemiBold : FontWeights.Normal, Foreground = isHeaderLine ? primaryText : secondaryText, TextWrapping = TextWrapping.Wrap, Margin = isHeaderLine ? new Thickness(0, 0, 0, 3) : new Thickness(0, 0, 0, 2), }); } if (!string.IsNullOrWhiteSpace(message.MetaRunId)) { stack.Children.Add(new TextBlock { Text = $"run {message.MetaRunId}", FontSize = 9.5, Foreground = secondaryText, Opacity = 0.7, Margin = new Thickness(0, 6, 0, 0), }); } wrapper.Child = stack; return wrapper; } }