using System.Windows; using System.Windows.Media; using System.Windows.Media.Animation; using AxCopilot.Services; using AxCopilot.Services.Agent; namespace AxCopilot.Views; public partial class ChatWindow { private Storyboard? _statusSpinStoryboard; private AppStateService.OperationalStatusPresentationState BuildOperationalStatusPresentation() { var hasLiveRuntimeActivity = !string.Equals(_activeTab, "Chat", StringComparison.OrdinalIgnoreCase) && (_runningConversationCount > 0 || _appState.ActiveTasks.Count > 0); return _appState.GetOperationalStatusPresentation( _activeTab, hasLiveRuntimeActivity, _runningConversationCount, _spotlightConversationCount, _runningOnlyFilter, _sortConversationsByRecent); } private void UpdateTaskSummaryIndicators() { var status = BuildOperationalStatusPresentation(); if (RuntimeActivityBadge != null) RuntimeActivityBadge.Visibility = status.ShowRuntimeBadge ? Visibility.Visible : Visibility.Collapsed; if (RuntimeActivityLabel != null) RuntimeActivityLabel.Text = status.RuntimeLabel; if (LastCompletedLabel != null) { LastCompletedLabel.Text = status.LastCompletedText; LastCompletedLabel.Visibility = status.ShowLastCompleted ? Visibility.Visible : Visibility.Collapsed; } if (ConversationStatusStrip != null && ConversationStatusStripLabel != null) { ConversationStatusStrip.Visibility = status.ShowCompactStrip ? Visibility.Visible : Visibility.Collapsed; ConversationStatusStripLabel.Text = status.ShowCompactStrip ? status.StripText : ""; if (status.ShowCompactStrip) { ConversationStatusStrip.Background = BrushFromHex(status.StripBackgroundHex); ConversationStatusStrip.BorderBrush = BrushFromHex(status.StripBorderHex); ConversationStatusStripLabel.Foreground = BrushFromHex(status.StripForegroundHex); } } UpdateConversationQuickStripUi(status); } private void UpdateConversationQuickStripUi() { UpdateConversationQuickStripUi(BuildOperationalStatusPresentation()); } private void UpdateConversationQuickStripUi(AppStateService.OperationalStatusPresentationState status) { if (ConversationQuickStrip == null || QuickRunningLabel == null || QuickHotLabel == null || BtnQuickRunningFilter == null || BtnQuickHotSort == null) return; ConversationQuickStrip.Visibility = status.ShowQuickStrip ? Visibility.Visible : Visibility.Collapsed; QuickRunningLabel.Text = status.QuickRunningText; QuickHotLabel.Text = status.QuickHotText; BtnQuickRunningFilter.Background = BrushFromHex(status.QuickRunningBackgroundHex); BtnQuickRunningFilter.BorderBrush = BrushFromHex(status.QuickRunningBorderHex); BtnQuickRunningFilter.BorderThickness = new Thickness(1); QuickRunningLabel.Foreground = BrushFromHex(status.QuickRunningForegroundHex); BtnQuickHotSort.Background = BrushFromHex(status.QuickHotBackgroundHex); BtnQuickHotSort.BorderBrush = BrushFromHex(status.QuickHotBorderHex); BtnQuickHotSort.BorderThickness = new Thickness(1); QuickHotLabel.Foreground = BrushFromHex(status.QuickHotForegroundHex); } private void SetStatus(string text, bool spinning) { if (StatusLabel != null) StatusLabel.Text = text; if (spinning) StartStatusAnimation(); else StopStatusAnimation(); } private static bool IsDecisionPending(string? summary) { var text = summary?.Trim() ?? ""; if (string.IsNullOrWhiteSpace(text)) return true; return text.Contains("확인 대기", StringComparison.OrdinalIgnoreCase) || text.Contains("승인 대기", StringComparison.OrdinalIgnoreCase); } private static bool IsDecisionApproved(string? summary) { var text = summary?.Trim() ?? ""; if (string.IsNullOrWhiteSpace(text)) return false; return text.Contains("계획 승인", StringComparison.OrdinalIgnoreCase); } private static bool IsDecisionRejected(string? summary) { var text = summary?.Trim() ?? ""; if (string.IsNullOrWhiteSpace(text)) return false; return text.Contains("계획 반려", StringComparison.OrdinalIgnoreCase) || text.Contains("수정 요청", StringComparison.OrdinalIgnoreCase) || text.Contains("취소", StringComparison.OrdinalIgnoreCase); } private static string GetDecisionStatusText(string? summary) { if (IsDecisionPending(summary)) return "계획 승인 대기 중"; if (IsDecisionApproved(summary)) return "계획 승인됨 · 실행 시작"; if (IsDecisionRejected(summary)) return "계획 반려됨 · 계획 재작성"; return string.IsNullOrWhiteSpace(summary) ? "사용자 의사결정 대기 중" : TruncateForStatus(summary); } private void SetStatusIdle() { StopStatusAnimation(); if (StatusLabel != null) StatusLabel.Text = "대기 중"; if (StatusElapsed != null) { StatusElapsed.Text = ""; StatusElapsed.Visibility = Visibility.Collapsed; } if (StatusTokens != null) { StatusTokens.Text = ""; StatusTokens.Visibility = Visibility.Collapsed; } RefreshContextUsageVisual(); ScheduleGitBranchRefresh(250); } private void UpdateStatusTokens(int inputTokens, int outputTokens) { if (StatusTokens == null) return; var llm = _settings.Settings.Llm; var (inCost, outCost) = Services.TokenEstimator.EstimateCost( inputTokens, outputTokens, llm.Service, llm.Model); var totalCost = inCost + outCost; var costText = totalCost > 0 ? $" · {Services.TokenEstimator.FormatCost(totalCost)}" : ""; StatusTokens.Text = $"↑{Services.TokenEstimator.Format(inputTokens)} ↓{Services.TokenEstimator.Format(outputTokens)}{costText}"; StatusTokens.Visibility = Visibility.Visible; RefreshContextUsageVisual(); } private void UpdateStatusBar(AgentEvent evt) { var toolLabel = evt.ToolName switch { "file_read" or "document_read" => "파일 읽기", "file_write" => "파일 쓰기", "file_edit" => "파일 수정", "html_create" => "HTML 생성", "xlsx_create" => "Excel 생성", "docx_create" => "Word 생성", "csv_create" => "CSV 생성", "md_create" => "Markdown 생성", "folder_map" => "폴더 탐색", "glob" => "파일 검색", "grep" => "내용 검색", "process" => "명령 실행", _ => evt.ToolName, }; var isDebugLogLevel = string.Equals(_settings.Settings.Llm.AgentLogLevel, "debug", StringComparison.OrdinalIgnoreCase); switch (evt.Type) { case AgentEventType.Thinking: SetStatus("생각 중...", spinning: true); break; case AgentEventType.Planning: SetStatus($"계획 수립 중 — {evt.StepTotal}단계", spinning: true); break; case AgentEventType.PermissionRequest: SetStatus($"권한 확인 중: {toolLabel}", spinning: false); break; case AgentEventType.PermissionGranted: SetStatus($"권한 승인됨: {toolLabel}", spinning: false); break; case AgentEventType.PermissionDenied: SetStatus($"권한 거부됨: {toolLabel}", spinning: false); StopStatusAnimation(); break; case AgentEventType.Decision: SetStatus(GetDecisionStatusText(evt.Summary), spinning: IsDecisionPending(evt.Summary)); break; case AgentEventType.ToolCall: if (!isDebugLogLevel) break; SetStatus($"{toolLabel} 실행 중...", spinning: true); break; case AgentEventType.ToolResult: SetStatus(evt.Success ? $"{toolLabel} 완료" : $"{toolLabel} 실패", spinning: false); break; case AgentEventType.StepStart: SetStatus($"[{evt.StepCurrent}/{evt.StepTotal}] {TruncateForStatus(evt.Summary)}", spinning: true); break; case AgentEventType.StepDone: SetStatus($"[{evt.StepCurrent}/{evt.StepTotal}] 단계 완료", spinning: true); break; case AgentEventType.SkillCall: if (!isDebugLogLevel) break; SetStatus($"스킬 실행 중: {TruncateForStatus(evt.Summary)}", spinning: true); break; case AgentEventType.Complete: SetStatus("작업 완료", spinning: false); StopStatusAnimation(); break; case AgentEventType.Error: SetStatus("오류 발생", spinning: false); StopStatusAnimation(); break; case AgentEventType.Paused: if (!isDebugLogLevel) break; SetStatus("⏸ 일시정지", spinning: false); break; case AgentEventType.Resumed: if (!isDebugLogLevel) break; SetStatus("▶ 재개됨", spinning: true); break; } } private void StartStatusAnimation() { if (_statusSpinStoryboard != null) return; var anim = new DoubleAnimation { From = 0, To = 360, Duration = TimeSpan.FromSeconds(2), RepeatBehavior = RepeatBehavior.Forever, }; _statusSpinStoryboard = new Storyboard(); Storyboard.SetTarget(anim, StatusDiamond); Storyboard.SetTargetProperty(anim, new PropertyPath("(UIElement.RenderTransform).(RotateTransform.Angle)")); _statusSpinStoryboard.Children.Add(anim); _statusSpinStoryboard.Begin(); } private void StopStatusAnimation() { _statusSpinStoryboard?.Stop(); _statusSpinStoryboard = null; } }