AX Agent 코워크·코드 흐름과 컨텍스트 관리를 claude-code 기준으로 대폭 정리
- 코워크·코드 프롬프트, 도구 선택, 문서 생성/검증 흐름을 claude-code 동등 품질 기준으로 재정렬함 - OpenAI/vLLM 경로의 오래된 tool history를 평탄화하고 최근 이력만 구조화해 컨텍스트 직렬화를 경량화함 - AX Agent UI를 테마 기준으로 재구성하고 플랜 승인/오버레이/이벤트 렌더링/명령 입력 상호작용을 개선함 - 파일 후보 제안, 반복 경로 정체 복구, LSP 보강, 문서·PPT 처리 개선, 설정/서비스 인터페이스 정리를 함께 반영함 - README.md 및 docs/DEVELOPMENT.md를 작업 시점별로 갱신함 - 검증: 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:
342
src/AxCopilot/Views/ChatWindow.V2LiveProgressPresentation.cs
Normal file
342
src/AxCopilot/Views/ChatWindow.V2LiveProgressPresentation.cs
Normal file
@@ -0,0 +1,342 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Threading;
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private DispatcherTimer? _v2LiveElapsedTimer;
|
||||
private DateTime _v2LiveStartTime;
|
||||
private TextBlock? _v2LiveElapsedText;
|
||||
|
||||
/// <summary>V2: 스트리밍 시작 시 라이브 진행 컨테이너 생성</summary>
|
||||
private void ShowAgentLiveCardV2(string runTab)
|
||||
{
|
||||
if (MessageList == null) return;
|
||||
if (!string.Equals(runTab, _activeTab, StringComparison.OrdinalIgnoreCase)) return;
|
||||
|
||||
RemoveAgentLiveCardV2(animated: false);
|
||||
|
||||
_v2LiveStartTime = DateTime.UtcNow;
|
||||
_v2LiveToolCards.Clear();
|
||||
_v2LastLiveToolCallId = null;
|
||||
|
||||
var msgMaxWidth = GetMessageMaxWidth();
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
|
||||
_v2LiveContainer = new StackPanel
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Width = msgMaxWidth,
|
||||
MaxWidth = msgMaxWidth,
|
||||
Margin = new Thickness(0, 4, 0, 6),
|
||||
};
|
||||
|
||||
// 에이전트 헤더 (아이콘 + 이름 + 경과시간)
|
||||
var headerGrid = new Grid { Margin = new Thickness(2, 0, 0, 4) };
|
||||
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 (agentName, _, _) = GetAgentIdentity();
|
||||
var (liveIconHost, livePixels, liveGlows, liveRotate, liveScale) = CreateMiniLauncherIconEx(4.0, "none");
|
||||
var canvas = liveIconHost.Children.OfType<Canvas>().FirstOrDefault();
|
||||
if (canvas != null)
|
||||
{
|
||||
var animState = new ChatIconAnimState
|
||||
{
|
||||
Host = liveIconHost, Canvas = canvas, Pixels = livePixels,
|
||||
Glows = liveGlows, Rotate = liveRotate, Scale = liveScale,
|
||||
IsRandomMode = _settings.Settings.Launcher.EnableChatIconRandomAnimation,
|
||||
};
|
||||
StartChatIconAnimation(animState);
|
||||
}
|
||||
|
||||
Grid.SetColumn(liveIconHost, 0);
|
||||
headerGrid.Children.Add(liveIconHost);
|
||||
|
||||
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);
|
||||
|
||||
_v2LiveElapsedText = new TextBlock
|
||||
{
|
||||
Text = "",
|
||||
FontSize = 10,
|
||||
Foreground = secondaryText,
|
||||
Opacity = 0.50,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(_v2LiveElapsedText, 2);
|
||||
headerGrid.Children.Add(_v2LiveElapsedText);
|
||||
|
||||
_v2LiveContainer.Children.Add(headerGrid);
|
||||
|
||||
AddTranscriptElement(_v2LiveContainer);
|
||||
ForceScrollToEnd();
|
||||
|
||||
// 경과 시간 타이머
|
||||
_v2LiveElapsedTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
_v2LiveElapsedTimer.Tick += (_, _) =>
|
||||
{
|
||||
if (_v2LiveElapsedText == null) return;
|
||||
var sec = (int)(DateTime.UtcNow - _v2LiveStartTime).TotalSeconds;
|
||||
_v2LiveElapsedText.Text = sec > 0 ? $"{sec}초 경과" : "";
|
||||
};
|
||||
_v2LiveElapsedTimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>V2: 에이전트 이벤트 수신 시 라이브 카드 업데이트</summary>
|
||||
private void UpdateAgentLiveCardV2(AgentEvent agentEvent)
|
||||
{
|
||||
if (_v2LiveContainer == null) return;
|
||||
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var hintBg = TryFindResource("HintBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var msgMaxWidth = GetMessageMaxWidth();
|
||||
|
||||
switch (agentEvent.Type)
|
||||
{
|
||||
case AgentEventType.ToolCall:
|
||||
{
|
||||
var (icon, iconColor) = GetV2ToolIcon(agentEvent.ToolName);
|
||||
var toolId = $"{agentEvent.ToolName}_{agentEvent.Timestamp.Ticks}";
|
||||
_v2LastLiveToolCallId = toolId;
|
||||
|
||||
var outerGrid = new Grid { Margin = new Thickness(0, 2, 0, 2) };
|
||||
outerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
outerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
|
||||
// 왼쪽 세로선 (진행중 = 맥박)
|
||||
var accentColor = ResolveLiveProgressAccentColor(
|
||||
TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue);
|
||||
var pulseLine = new Border
|
||||
{
|
||||
Width = 2,
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x80, accentColor.R, accentColor.G, accentColor.B)),
|
||||
CornerRadius = new CornerRadius(1),
|
||||
Margin = new Thickness(12, 0, 8, 0),
|
||||
};
|
||||
// 맥박 애니메이션
|
||||
var pulseAnim = new DoubleAnimation(0.4, 1.0, TimeSpan.FromMilliseconds(800))
|
||||
{
|
||||
AutoReverse = true,
|
||||
RepeatBehavior = RepeatBehavior.Forever,
|
||||
EasingFunction = new SineEase(),
|
||||
};
|
||||
pulseLine.BeginAnimation(UIElement.OpacityProperty, pulseAnim);
|
||||
Grid.SetColumn(pulseLine, 0);
|
||||
outerGrid.Children.Add(pulseLine);
|
||||
|
||||
var card = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x10, accentColor.R, accentColor.G, accentColor.B)),
|
||||
BorderBrush = new SolidColorBrush(Color.FromArgb(0x30, accentColor.R, accentColor.G, accentColor.B)),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(10, 6, 10, 6),
|
||||
Tag = "pending",
|
||||
};
|
||||
Grid.SetColumn(card, 1);
|
||||
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontSize = 12,
|
||||
Foreground = iconColor,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
});
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = GetV2ToolDisplayName(agentEvent.ToolName),
|
||||
FontSize = 11,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
if (!string.IsNullOrWhiteSpace(agentEvent.FilePath))
|
||||
{
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = TruncateFilePath(agentEvent.FilePath, 50),
|
||||
FontSize = 10,
|
||||
Foreground = secondaryText,
|
||||
Opacity = 0.7,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(8, 0, 0, 0),
|
||||
});
|
||||
}
|
||||
// 스피너 대신 "..." 텍스트
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "...",
|
||||
FontSize = 11,
|
||||
Foreground = secondaryText,
|
||||
Opacity = 0.5,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(6, 0, 0, 0),
|
||||
});
|
||||
card.Child = sp;
|
||||
outerGrid.Children.Add(card);
|
||||
|
||||
_v2LiveToolCards[toolId] = card;
|
||||
_v2LiveContainer.Children.Add(outerGrid);
|
||||
ForceScrollToEnd();
|
||||
break;
|
||||
}
|
||||
|
||||
case AgentEventType.ToolResult:
|
||||
{
|
||||
// 마지막 pending 카드를 완료 상태로 변환
|
||||
if (_v2LastLiveToolCallId != null && _v2LiveToolCards.TryGetValue(_v2LastLiveToolCallId, out var pendingCard))
|
||||
{
|
||||
var isSuccess = agentEvent.Success;
|
||||
var statusIcon = isSuccess ? "\uE73E" : "\uE711";
|
||||
var statusColor = isSuccess
|
||||
? new SolidColorBrush(Color.FromRgb(0x66, 0xBB, 0x6A))
|
||||
: new SolidColorBrush(Color.FromRgb(0xEF, 0x53, 0x50));
|
||||
|
||||
// 카드 배경/테두리를 성공/실패 색으로 변경
|
||||
pendingCard.Background = isSuccess
|
||||
? new SolidColorBrush(Color.FromArgb(0x0A, 0x66, 0xBB, 0x6A))
|
||||
: new SolidColorBrush(Color.FromArgb(0x0A, 0xEF, 0x53, 0x50));
|
||||
pendingCard.BorderBrush = isSuccess
|
||||
? new SolidColorBrush(Color.FromArgb(0x30, 0x66, 0xBB, 0x6A))
|
||||
: new SolidColorBrush(Color.FromArgb(0x30, 0xEF, 0x53, 0x50));
|
||||
pendingCard.Tag = "complete";
|
||||
|
||||
// "..." 텍스트를 상태 아이콘 + 소요시간으로 교체
|
||||
if (pendingCard.Child is StackPanel sp)
|
||||
{
|
||||
// 마지막 "..." 제거
|
||||
if (sp.Children.Count > 0 && sp.Children[^1] is TextBlock lastTb && lastTb.Text == "...")
|
||||
sp.Children.RemoveAt(sp.Children.Count - 1);
|
||||
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = statusIcon,
|
||||
FontFamily = s_segoeIconFont,
|
||||
FontSize = 11,
|
||||
Foreground = statusColor,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(6, 0, 0, 0),
|
||||
});
|
||||
|
||||
var elapsed = NormalizeProgressElapsedMs(agentEvent.ElapsedMs);
|
||||
if (elapsed > 0)
|
||||
{
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"{elapsed / 1000.0:F1}s",
|
||||
FontSize = 10,
|
||||
Foreground = secondaryText,
|
||||
Opacity = 0.6,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(4, 0, 0, 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 세로선 맥박 애니메이션 중지
|
||||
var parent = pendingCard.Parent as Grid;
|
||||
if (parent?.Children[0] is Border pulseLine)
|
||||
{
|
||||
pulseLine.BeginAnimation(UIElement.OpacityProperty, null);
|
||||
pulseLine.Opacity = 1;
|
||||
pulseLine.Background = isSuccess
|
||||
? new SolidColorBrush(Color.FromArgb(0x60, 0x66, 0xBB, 0x6A))
|
||||
: new SolidColorBrush(Color.FromArgb(0x60, 0xEF, 0x53, 0x50));
|
||||
}
|
||||
|
||||
_v2LastLiveToolCallId = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case AgentEventType.Thinking:
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(agentEvent.Summary)) break;
|
||||
|
||||
// 사고 과정을 간략히 표시
|
||||
var thinkRow = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Margin = new Thickness(22, 2, 0, 2),
|
||||
};
|
||||
thinkRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE915",
|
||||
FontFamily = s_segoeIconFont,
|
||||
FontSize = 10,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0x59, 0xA5, 0xF5)),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
});
|
||||
var thinkText = agentEvent.Summary;
|
||||
if (thinkText.Length > 100) thinkText = thinkText[..100] + "...";
|
||||
thinkRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = thinkText,
|
||||
FontSize = 10.5,
|
||||
FontStyle = FontStyles.Italic,
|
||||
Foreground = secondaryText,
|
||||
Opacity = 0.65,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
MaxWidth = msgMaxWidth - 60,
|
||||
});
|
||||
_v2LiveContainer.Children.Add(thinkRow);
|
||||
ForceScrollToEnd();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>V2: 스트리밍 종료 시 라이브 카드 제거</summary>
|
||||
private void RemoveAgentLiveCardV2(bool animated = true)
|
||||
{
|
||||
_v2LiveElapsedTimer?.Stop();
|
||||
_v2LiveElapsedTimer = null;
|
||||
_v2LiveElapsedText = null;
|
||||
|
||||
if (_v2LiveContainer == null) return;
|
||||
|
||||
// 아이콘 애니메이션 상태 정리
|
||||
_chatIconAnimStates.RemoveAll(s => s.Host != null && !s.Host.IsVisible);
|
||||
|
||||
var toRemove = _v2LiveContainer;
|
||||
_v2LiveContainer = null;
|
||||
_v2LiveToolCards.Clear();
|
||||
_v2LastLiveToolCallId = null;
|
||||
|
||||
if (animated && ContainsTranscriptElement(toRemove))
|
||||
{
|
||||
var anim = new DoubleAnimation(toRemove.Opacity, 0, TimeSpan.FromMilliseconds(160));
|
||||
anim.Completed += (_, _) => RemoveTranscriptElement(toRemove);
|
||||
toRemove.BeginAnimation(UIElement.OpacityProperty, anim);
|
||||
return;
|
||||
}
|
||||
|
||||
RemoveTranscriptElement(toRemove);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user