구조 개선: 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
This commit is contained in:
@@ -3530,6 +3530,7 @@ public partial class ChatWindow : Window
|
||||
|
||||
private void ShowDropActionMenu(string[] files)
|
||||
{
|
||||
if (files == null || files.Length == 0) return;
|
||||
// 파일 유형 판별
|
||||
var ext = System.IO.Path.GetExtension(files[0]).ToLowerInvariant();
|
||||
string category;
|
||||
@@ -4243,7 +4244,9 @@ public partial class ChatWindow : Window
|
||||
if (string.IsNullOrWhiteSpace(raw) || string.Equals(raw, command, StringComparison.OrdinalIgnoreCase))
|
||||
return ("open", "");
|
||||
var parts = raw.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length <= 1)
|
||||
if (parts.Length == 0)
|
||||
return ("open", "");
|
||||
if (parts.Length == 1)
|
||||
return (parts[0].Trim().ToLowerInvariant(), "");
|
||||
return (parts[0].Trim().ToLowerInvariant(), string.Join(' ', parts.Skip(1)).Trim());
|
||||
}
|
||||
@@ -5515,7 +5518,8 @@ public partial class ChatWindow : Window
|
||||
}
|
||||
|
||||
private AgentLoopService GetAgentLoop(string tab) =>
|
||||
_agentLoops.TryGetValue(tab, out var loop) ? loop : _agentLoops.Values.First();
|
||||
_agentLoops.TryGetValue(tab, out var loop) ? loop : _agentLoops.Values.FirstOrDefault()
|
||||
?? throw new InvalidOperationException($"No agent loop registered for tab '{tab}'");
|
||||
|
||||
private async Task<string> RunAgentLoopAsync(
|
||||
string runTab,
|
||||
@@ -6840,200 +6844,6 @@ public partial class ChatWindow : Window
|
||||
|
||||
// ─── 채팅창 내 에이전트 라이브 진행 카드 ─────────────────────────────────────
|
||||
|
||||
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 accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
|
||||
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 System.Windows.Media.Animation.QuadraticEase
|
||||
{ EasingMode = System.Windows.Media.Animation.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 = System.Windows.Media.Animation.RepeatBehavior.Forever,
|
||||
EasingFunction = new System.Windows.Media.Animation.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 System.Windows.Media.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)
|
||||
{
|
||||
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))
|
||||
{
|
||||
_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 System.Windows.Media.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);
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveTranscriptElement(toRemove);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 미니 다이아몬드 아이콘 애니메이션 ───────────────────────────────────────
|
||||
|
||||
private void StartStatusDiamondAnimation()
|
||||
|
||||
Reference in New Issue
Block a user