Files
AX-Copilot-Codex/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs
lacvet dc2574bb17
Some checks failed
Release Gate / gate (push) Has been cancelled
AX Agent 진행 표시를 Claude Code 스타일에 가깝게 정리
- execution history를 접은 상태에서도 대기/압축/중요 진행 이벤트가 transcript에 계속 노출되도록 필터를 조정함

- 진행 줄 메타를 경과 시간 · 누적 토큰 형식으로 통일하고 일반 진행 이벤트를 평평한 line 스타일로 정리함

- 장기 대기/컨텍스트 압축 상태만 강조 배경과 펄스 마커를 유지해 살아 있는 작업이 더 잘 보이도록 개선함

- README와 DEVELOPMENT 문서에 2026-04-06 23:26 (KST) 기준 이력을 반영함

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

262 lines
9.6 KiB
C#

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<ChatMessage> 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<ChatMessage>();
}
private List<ChatExecutionEvent> GetVisibleTimelineEvents(ChatConversation? conversation)
{
var events = conversation?.ExecutionEvents?.ToList() ?? new List<ChatExecutionEvent>();
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<ChatMessage> visibleMessages,
IReadOnlyCollection<ChatExecutionEvent> 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<AgentEventType>(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;
}
}