From fa431f1666595eb0644aad2f046986473613bd33 Mon Sep 17 00:00:00 2001 From: lacvet Date: Mon, 6 Apr 2026 10:42:01 +0900 Subject: [PATCH] =?UTF-8?q?AX=20Agent=20timeline=20=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=A0=A0=ED=85=8C=EC=9D=B4=EC=85=98=20helper=EB=A5=BC=20?= =?UTF-8?q?=ED=95=9C=20=ED=8C=8C=EC=9D=BC=EB=A1=9C=20=EB=AA=A8=EC=95=84=20?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=20=EC=B1=85=EC=9E=84=EC=9D=84=20=EB=8D=94=20?= =?UTF-8?q?=EC=A4=84=EC=9D=B8=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CreateTimelineLoadMoreCard, ToAgentEvent, IsCompactionMetaMessage, CreateCompactionMetaCard를 ChatWindow.TimelinePresentation.cs로 이동해 RenderMessages 주변의 timeline 관련 helper를 하나의 presentation surface로 정리했다. README와 DEVELOPMENT 문서에 2026-04-06 10:44 (KST) 기준 이력을 반영했고, dotnet build 검증 결과 경고 0 / 오류 0을 확인했다. --- README.md | 2 + docs/DEVELOPMENT.md | 2 + .../Views/ChatWindow.TimelinePresentation.cs | 182 ++++++++++++++++++ src/AxCopilot/Views/ChatWindow.xaml.cs | 178 ----------------- 4 files changed, 186 insertions(+), 178 deletions(-) diff --git a/README.md b/README.md index 6d3ff0c..1c959d1 100644 --- a/README.md +++ b/README.md @@ -1186,3 +1186,5 @@ MIT License - 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에서 관리되게 정리했다. - 업데이트: 2026-04-06 10:36 (KST) - timeline 조립 helper를 [ChatWindow.TimelinePresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs) 로 분리했다. `RenderMessages()`가 직접 처리하던 visible 메시지 필터링, execution event 노출 집계, timestamp/order 기반 timeline action 조립을 helper 메서드로 옮겨 메인 렌더 루프를 더 단순화했다. +- 업데이트: 2026-04-06 10:44 (KST) + - timeline presentation 정리를 이어서 진행했다. [ChatWindow.TimelinePresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs) 에 `CreateTimelineLoadMoreCard`, `ToAgentEvent`, `IsCompactionMetaMessage`, `CreateCompactionMetaCard`까지 옮겨 `RenderMessages()` 주변의 timeline helper를 한 파일로 모았다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 13a62b4..3e85b4f 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4927,3 +4927,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - 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. - Document update: 2026-04-06 10:36 (KST) - Split timeline visibility filtering and render-action assembly out of `RenderMessages()` into `ChatWindow.TimelinePresentation.cs`. Visible message/event selection and timestamp-ordered timeline action construction now live in dedicated helpers. - Document update: 2026-04-06 10:36 (KST) - This keeps `RenderMessages()` closer to a simple orchestration loop and reduces mixed responsibilities inside the main chat window file. +- Document update: 2026-04-06 10:44 (KST) - Continued timeline presentation cleanup by moving `CreateTimelineLoadMoreCard`, `ToAgentEvent`, `IsCompactionMetaMessage`, and `CreateCompactionMetaCard` into `ChatWindow.TimelinePresentation.cs`. +- Document update: 2026-04-06 10:44 (KST) - This consolidates timeline-related helpers in one place and leaves the main chat window file with less transcript-specific rendering logic around `RenderMessages()`. diff --git a/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs b/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs index 6b18e32..812c80e 100644 --- a/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs +++ b/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs @@ -1,7 +1,11 @@ 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; @@ -49,4 +53,182 @@ public partial class ChatWindow .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; + } } diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 428b510..e91faf7 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -2770,184 +2770,6 @@ public partial class ChatWindow : Window }, DispatcherPriority.Background); } - 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 = isHeaderLine ? 10.5 : 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; - } - // ─── 커스텀 체크 아이콘 (모든 팝업 메뉴 공통) ───────────────────────── /// 커스텀 체크/미선택 아이콘을 생성합니다. Path 도형 기반, 선택 시 스케일 바운스 애니메이션.