Files
AX-Copilot-Codex/src/AxCopilot/Views/ChatWindow.LiveProgressPresentation.cs
lacvet 7931566212 구조 개선: transcript 지연 가상화와 tool executor 분리 적용
이번 변경은 claude-code 기준 구조 강건성을 높이기 위한 리팩터링입니다.

핵심 수정 사항:

- AX Agent transcript를 TranscriptVisualItem/TranscriptVisualHost 기반 지연 materialization 구조로 전환해 MessageList 가상화 기반을 강화했습니다.

- StreamingToolExecutionCoordinator를 IToolExecutionCoordinator 뒤로 분리해 AgentLoopService가 구체 구현에 직접 묶이지 않도록 정리했습니다.

- 라이브 진행 카드 렌더를 ChatWindow.LiveProgressPresentation partial로 이동해 ChatWindow.xaml.cs의 책임을 더 줄였습니다.

- 기존 메시지 bubble 조립 로직을 유지하면서 transcript host가 필요 시점에 bubble을 만들 수 있도록 helper 경로를 추가했습니다.

검증 결과:

- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\

- 경고 0 / 오류 0
2026-04-09 01:37:08 +09:00

209 lines
7.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
namespace AxCopilot.Views;
public partial class ChatWindow
{
private void ShowAgentLiveCard(string runTab)
{
if (MessageList == null) return;
if (!string.Equals(runTab, _activeTab, StringComparison.OrdinalIgnoreCase)) return;
RemoveAgentLiveCard(animated: false);
_agentLiveStartTime = DateTime.UtcNow;
_agentLiveSubItemTexts.Clear();
_agentLiveCurrentCategory = null;
var msgMaxWidth = GetMessageMaxWidth();
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var container = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
Width = msgMaxWidth,
MaxWidth = msgMaxWidth,
Margin = new Thickness(0, 4, 0, 6),
Opacity = IsLightweightLiveProgressMode(runTab) ? 1 : 0,
RenderTransform = IsLightweightLiveProgressMode(runTab)
? Transform.Identity
: new TranslateTransform(0, 8),
};
if (!IsLightweightLiveProgressMode(runTab))
{
container.BeginAnimation(UIElement.OpacityProperty,
new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(260)));
((TranslateTransform)container.RenderTransform).BeginAnimation(
TranslateTransform.YProperty,
new DoubleAnimation(8, 0, TimeSpan.FromMilliseconds(280))
{
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }
});
}
var headerGrid = new Grid { Margin = new Thickness(2, 0, 0, 3) };
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
var liveIcon = CreateMiniLauncherIcon(pixelSize: 4.0);
if (!IsLightweightLiveProgressMode(runTab))
{
liveIcon.BeginAnimation(
UIElement.OpacityProperty,
new DoubleAnimation(1.0, 0.35, TimeSpan.FromMilliseconds(750))
{
AutoReverse = true,
RepeatBehavior = RepeatBehavior.Forever,
EasingFunction = new SineEase()
});
}
Grid.SetColumn(liveIcon, 0);
headerGrid.Children.Add(liveIcon);
var (agentName, _, _) = GetAgentIdentity();
var nameTb = new TextBlock
{
Text = agentName,
FontSize = 11,
FontWeight = FontWeights.SemiBold,
Foreground = secondaryText,
Margin = new Thickness(6, 0, 0, 0),
VerticalAlignment = VerticalAlignment.Center,
};
Grid.SetColumn(nameTb, 1);
headerGrid.Children.Add(nameTb);
_agentLiveElapsedText = new TextBlock
{
Text = "",
FontSize = 10,
Foreground = secondaryText,
Opacity = 0.50,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
};
Grid.SetColumn(_agentLiveElapsedText, 2);
headerGrid.Children.Add(_agentLiveElapsedText);
container.Children.Add(headerGrid);
var card = new Border
{
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(12),
Padding = new Thickness(13, 10, 13, 10),
};
card.SetResourceReference(Border.BackgroundProperty, "ItemBackground");
var cardStack = new StackPanel();
_agentLiveStatusText = new TextBlock
{
Text = "준비 중...",
FontSize = 12,
FontFamily = new FontFamily("Segoe UI, Malgun Gothic"),
Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap,
};
cardStack.Children.Add(_agentLiveStatusText);
_agentLiveSubItems = new StackPanel { Margin = new Thickness(0, 6, 0, 0) };
cardStack.Children.Add(_agentLiveSubItems);
card.Child = cardStack;
container.Children.Add(card);
_agentLiveContainer = container;
AddTranscriptElement(container);
ForceScrollToEnd();
_agentLiveElapsedTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_agentLiveElapsedTimer.Tick += (_, _) =>
{
if (_agentLiveElapsedText == null)
return;
var sec = (int)(DateTime.UtcNow - _agentLiveStartTime).TotalSeconds;
_agentLiveElapsedText.Text = sec > 0 ? $"{sec}초 경과" : "";
};
_agentLiveElapsedTimer.Start();
}
private void UpdateAgentLiveCard(string message, string? subItem = null,
string? category = null, bool clearSubItems = false)
{
if (_agentLiveContainer == null || _agentLiveStatusText == null) return;
_agentLiveStatusText.Text = message;
if (clearSubItems || (category != null && category != _agentLiveCurrentCategory))
{
_agentLiveSubItemTexts.Clear();
_agentLiveSubItems?.Children.Clear();
if (category != null)
_agentLiveCurrentCategory = category;
}
if (string.IsNullOrEmpty(subItem) || _agentLiveSubItemTexts.Contains(subItem))
return;
_agentLiveSubItemTexts.Add(subItem);
const int maxLiveSubItems = 8;
if (_agentLiveSubItemTexts.Count > maxLiveSubItems)
{
_agentLiveSubItemTexts.RemoveAt(0);
if (_agentLiveSubItems?.Children.Count > 0)
_agentLiveSubItems.Children.RemoveAt(0);
}
var secondary = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var tb = new TextBlock
{
Text = $" {subItem}",
FontSize = 10.5,
FontFamily = new FontFamily("Segoe UI, Malgun Gothic"),
Foreground = secondary,
Opacity = 0.62,
TextTrimming = TextTrimming.CharacterEllipsis,
MaxWidth = 520,
Margin = new Thickness(0, 1, 0, 0),
};
_agentLiveSubItems?.Children.Add(tb);
ForceScrollToEnd();
}
private void RemoveAgentLiveCard(bool animated = true)
{
_agentLiveElapsedTimer?.Stop();
_agentLiveElapsedTimer = null;
if (_agentLiveContainer == null)
return;
var toRemove = _agentLiveContainer;
_agentLiveContainer = null;
_agentLiveStatusText = null;
_agentLiveSubItems = null;
_agentLiveElapsedText = null;
_agentLiveSubItemTexts.Clear();
_agentLiveCurrentCategory = null;
if (animated && ContainsTranscriptElement(toRemove) && !IsLightweightLiveProgressMode())
{
var anim = new DoubleAnimation(toRemove.Opacity, 0, TimeSpan.FromMilliseconds(160));
anim.Completed += (_, _) => RemoveTranscriptElement(toRemove);
toRemove.BeginAnimation(UIElement.OpacityProperty, anim);
return;
}
RemoveTranscriptElement(toRemove);
}
}