Files
AX-Copilot-Codex/src/AxCopilot/Views/ChatWindow.ContextUsagePresentation.cs
lacvet da11029284 claude-code 기준 provider 호환성과 compact 후속 흐름을 보강한다
- OpenAI 호환 tool_choice 400 오류에 대한 일반 fallback을 추가하고 Qwen·LLaMA·DeepSeek 계열 vLLM의 도구 호출 프로파일을 더 보수적으로 조정

- compact 이후 branch context와 최근 tool state를 query view에 재주입하고 UI 표현 수준에 맞춰 compact 카드/컨텍스트 사용 팝업/최종 보고 밀도를 세분화

- README와 DEVELOPMENT 문서 이력을 2026-04-12 23:45 KST 기준으로 갱신

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
2026-04-12 22:32:40 +09:00

196 lines
7.5 KiB
C#

using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace AxCopilot.Views;
public partial class ChatWindow
{
// 메시지 개수와 대화 ID가 바뀔 때만 재계산한다.
private int _cachedMessageTokens;
private int _cachedMessageCountForTokens = -1;
private string? _cachedConvIdForTokens;
private void RefreshContextUsageVisual()
{
if (TokenUsageCard == null || TokenUsageArc == null || TokenUsagePercentText == null
|| TokenUsageSummaryText == null || TokenUsageHintText == 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 expressionLevel = GetAgentUiExpressionLevel();
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)
{
var convId = _currentConversation?.Id;
var msgCount = _currentConversation?.Messages?.Count ?? 0;
if (convId != _cachedConvIdForTokens || msgCount != _cachedMessageCountForTokens || _isStreaming)
{
_cachedMessageTokens = msgCount > 0
? Services.TokenEstimator.EstimateMessages(_currentConversation!.Messages)
: 0;
_cachedConvIdForTokens = convId;
_cachedMessageCountForTokens = msgCount;
}
messageTokens = _cachedMessageTokens;
}
var draftText = InputBox?.Text ?? "";
var draftTokens = string.IsNullOrWhiteSpace(draftText) ? 0 : Services.TokenEstimator.Estimate(draftText) + 4;
var hasAnyMessages = messageTokens > 0 || _isStreaming;
var baseOverhead = 0;
if (hasAnyMessages)
{
var sysPromptLen = _llm?.SystemPrompt?.Length ?? 0;
var toolCount = _toolRegistry?.GetActiveToolsForTab(_activeTab ?? "Chat")?.Count ?? 0;
baseOverhead = Services.TokenEstimator.EstimateBaseOverhead(sysPromptLen, toolCount);
}
var currentTokens = Math.Max(0, messageTokens + draftTokens + baseOverhead);
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;
var percentText = $"{System.Math.Round(usageRatio * 100):0}%";
TokenUsagePercentText.Text = percentText;
TokenUsageSummaryText.Text = $"컨텍스트 {percentText}";
TokenUsageHintText.Text = $"{Services.TokenEstimator.Format(currentTokens)} / {Services.TokenEstimator.Format(maxContextTokens)}";
CompactNowLabel.Text = compactLabel;
ViewModel.ContextTokens = currentTokens;
ViewModel.MaxContextTokens = maxContextTokens;
if (TokenUsagePopupTitle != null)
TokenUsagePopupTitle.Text = $"컨텍스트 창 {percentText}";
if (TokenUsagePopupUsage != null)
TokenUsagePopupUsage.Text = $"{Services.TokenEstimator.Format(currentTokens)}/{Services.TokenEstimator.Format(maxContextTokens)}";
if (TokenUsagePopupDetail != null)
TokenUsagePopupDetail.Text = expressionLevel == "simple"
? detailText
: $"{detailText} · 임계 {triggerPercent}%";
if (TokenUsagePopupCompact != null)
{
TokenUsagePopupCompact.Text = _sessionCompactionCount > 0
? expressionLevel switch
{
"simple" => $"압축 {_sessionCompactionCount}회 · {FormatTokenCount(_sessionCompactionSavedTokens)} 절감",
_ => $"누적 압축 {_sessionCompactionCount}회 · 절감 {FormatTokenCount(_sessionCompactionSavedTokens)}"
}
: expressionLevel == "rich"
? "AX Agent가 컨텍스트를 자동 관리합니다"
: "컨텍스트 자동 관리";
}
TokenUsageCard.ToolTip = null;
UpdateCircularUsageArc(TokenUsageArc, usageRatio, 15, 15, 11);
}
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;
}
}
}