Some checks failed
Release Gate / gate (push) Has been cancelled
- 유휴 전환 후 하단 상태바 전체 토큰 집계가 사라지지 않도록 대화 기준 합산 복원 경로 추가 - 컨텍스트 사용량 팝업에 마지막 실제 압축 before/after 및 누적 절감량 표시 - total_stats 이벤트가 진행 피드에 흡수되지 않고 전용 통계 카드로 다시 노출되게 수정 - 관련 README 및 개발 문서 이력 갱신 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ (경고 0, 오류 0)
160 lines
6.2 KiB
C#
160 lines
6.2 KiB
C#
using System.Windows;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
|
|
namespace AxCopilot.Views;
|
|
|
|
public partial class ChatWindow
|
|
{
|
|
private void RefreshContextUsageVisual()
|
|
{
|
|
if (TokenUsageCard == null || TokenUsageArc == null || TokenUsagePercentText == null
|
|
|| TokenUsageSummaryText == null || TokenUsageHintText == null
|
|
|| TokenUsageThresholdMarker == null || CompactNowLabel == null)
|
|
return;
|
|
|
|
var showContextUsage = _activeTab is "Cowork" or "Code";
|
|
TokenUsageCard.Visibility = showContextUsage ? Visibility.Visible : Visibility.Collapsed;
|
|
if (!showContextUsage)
|
|
{
|
|
if (TokenUsagePopup != null)
|
|
TokenUsagePopup.IsOpen = false;
|
|
return;
|
|
}
|
|
|
|
var llm = _settings.Settings.Llm;
|
|
var maxContextTokens = Math.Clamp(llm.MaxContextTokens, 1024, 1_000_000);
|
|
var triggerPercent = Math.Clamp(llm.ContextCompactTriggerPercent, 10, 95);
|
|
var triggerRatio = triggerPercent / 100.0;
|
|
|
|
int messageTokens;
|
|
lock (_convLock)
|
|
messageTokens = _currentConversation?.Messages?.Count > 0
|
|
? Services.TokenEstimator.EstimateMessages(_currentConversation.Messages)
|
|
: 0;
|
|
|
|
var draftText = InputBox?.Text ?? "";
|
|
var draftTokens = string.IsNullOrWhiteSpace(draftText) ? 0 : Services.TokenEstimator.Estimate(draftText) + 4;
|
|
var currentTokens = Math.Max(0, messageTokens + draftTokens);
|
|
var usageRatio = Services.TokenEstimator.GetContextUsage(currentTokens, maxContextTokens);
|
|
|
|
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue;
|
|
Brush progressBrush = accentBrush;
|
|
string summary;
|
|
string compactLabel;
|
|
|
|
if (usageRatio >= 1.0)
|
|
{
|
|
progressBrush = Brushes.IndianRed;
|
|
summary = "컨텍스트 한도 초과";
|
|
compactLabel = "지금 압축";
|
|
}
|
|
else if (usageRatio >= triggerRatio)
|
|
{
|
|
progressBrush = Brushes.DarkOrange;
|
|
summary = llm.EnableProactiveContextCompact ? "곧 자동 압축" : "압축 한계 도달";
|
|
compactLabel = "압축 권장";
|
|
}
|
|
else if (usageRatio >= triggerRatio * 0.7)
|
|
{
|
|
progressBrush = Brushes.Goldenrod;
|
|
summary = "컨텍스트 사용 증가";
|
|
compactLabel = "미리 압축";
|
|
}
|
|
else
|
|
{
|
|
summary = "컨텍스트 여유";
|
|
compactLabel = "압축";
|
|
}
|
|
|
|
string detailText;
|
|
if (_lastCompactionAt.HasValue && _lastCompactionBeforeTokens.HasValue && _lastCompactionAfterTokens.HasValue)
|
|
{
|
|
var compactType = _lastCompactionWasAutomatic ? "자동" : "수동";
|
|
detailText = $"{compactType} 압축 {Services.TokenEstimator.Format(_lastCompactionBeforeTokens.Value)} → {Services.TokenEstimator.Format(_lastCompactionAfterTokens.Value)}";
|
|
}
|
|
else
|
|
{
|
|
detailText = $"자동 압축 시작 {triggerPercent}%";
|
|
}
|
|
|
|
TokenUsageArc.Stroke = progressBrush;
|
|
TokenUsageThresholdMarker.Fill = progressBrush;
|
|
var percentText = $"{Math.Round(usageRatio * 100):0}%";
|
|
TokenUsagePercentText.Text = percentText;
|
|
TokenUsageSummaryText.Text = $"컨텍스트 {percentText}";
|
|
TokenUsageHintText.Text = $"{Services.TokenEstimator.Format(currentTokens)} / {Services.TokenEstimator.Format(maxContextTokens)}";
|
|
CompactNowLabel.Text = compactLabel;
|
|
|
|
if (TokenUsagePopupTitle != null)
|
|
TokenUsagePopupTitle.Text = $"컨텍스트 창 {percentText}";
|
|
if (TokenUsagePopupUsage != null)
|
|
TokenUsagePopupUsage.Text = $"{Services.TokenEstimator.Format(currentTokens)}/{Services.TokenEstimator.Format(maxContextTokens)}";
|
|
if (TokenUsagePopupDetail != null)
|
|
TokenUsagePopupDetail.Text = _pendingPostCompaction
|
|
? $"compact 후 첫 응답 대기 중 · {detailText}"
|
|
: detailText;
|
|
if (TokenUsagePopupCompact != null)
|
|
TokenUsagePopupCompact.Text = _sessionCompactionCount > 0
|
|
? $"누적 압축 {_sessionCompactionCount}회 · 절감 {FormatTokenCount(_sessionCompactionSavedTokens)} tokens"
|
|
: "AX Agent가 컨텍스트를 자동으로 관리합니다";
|
|
|
|
TokenUsageCard.ToolTip = null;
|
|
|
|
UpdateCircularUsageArc(TokenUsageArc, usageRatio, 14, 14, 11);
|
|
PositionThresholdMarker(TokenUsageThresholdMarker, triggerRatio, 14, 14, 11, 2.5);
|
|
}
|
|
|
|
private void TokenUsageCard_MouseEnter(object sender, MouseEventArgs e)
|
|
{
|
|
_tokenUsagePopupCloseTimer.Stop();
|
|
if (TokenUsagePopup != null && TokenUsageCard?.Visibility == Visibility.Visible)
|
|
TokenUsagePopup.IsOpen = true;
|
|
}
|
|
|
|
private void TokenUsageCard_MouseLeave(object sender, MouseEventArgs e)
|
|
{
|
|
_tokenUsagePopupCloseTimer.Stop();
|
|
_tokenUsagePopupCloseTimer.Start();
|
|
}
|
|
|
|
private void TokenUsagePopup_MouseEnter(object sender, MouseEventArgs e)
|
|
{
|
|
_tokenUsagePopupCloseTimer.Stop();
|
|
}
|
|
|
|
private void TokenUsagePopup_MouseLeave(object sender, MouseEventArgs e)
|
|
{
|
|
_tokenUsagePopupCloseTimer.Stop();
|
|
_tokenUsagePopupCloseTimer.Start();
|
|
}
|
|
|
|
private void CloseTokenUsagePopupIfIdle()
|
|
{
|
|
if (TokenUsagePopup == null)
|
|
return;
|
|
|
|
var cardHovered = IsMouseInsideElement(TokenUsageCard);
|
|
var popupHovered = TokenUsagePopup.Child is FrameworkElement popupChild && IsMouseInsideElement(popupChild);
|
|
if (!cardHovered && !popupHovered)
|
|
TokenUsagePopup.IsOpen = false;
|
|
}
|
|
|
|
private static bool IsMouseInsideElement(FrameworkElement? element)
|
|
{
|
|
if (element == null || !element.IsVisible || element.ActualWidth <= 0 || element.ActualHeight <= 0)
|
|
return false;
|
|
|
|
try
|
|
{
|
|
var mouse = System.Windows.Forms.Control.MousePosition;
|
|
var point = element.PointFromScreen(new Point(mouse.X, mouse.Y));
|
|
return point.X >= 0 && point.Y >= 0 && point.X <= element.ActualWidth && point.Y <= element.ActualHeight;
|
|
}
|
|
catch
|
|
{
|
|
return element.IsMouseOver;
|
|
}
|
|
}
|
|
}
|