AX Agent 계획 승인 흐름과 상태선 표현 계층 정리
Some checks failed
Release Gate / gate (push) Has been cancelled

- 계획 승인 기본 흐름을 transcript inline 우선 구조로 더 정리하고, 계획 버튼은 저장된 계획을 여는 상세 보기 성격으로 분리했습니다.

- OperationalStatusPresentationState를 확장해 runtime badge, compact strip, quick strip의 문구·강조색·노출 여부를 한 번에 계산하도록 통합했습니다.

- ChatWindow 상태선/quick strip/status token 로직을 StatusPresentation partial로 분리해 메인 창 코드의 직접 분기와 렌더 책임을 줄였습니다.

- 문서 이력(README, DEVELOPMENT)을 2026-04-06 01:37 KST 기준으로 갱신했습니다.

- 검증: 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:
2026-04-06 07:17:16 +09:00
parent 95e40df354
commit 90bd77f945
7 changed files with 290 additions and 165 deletions

View File

@@ -69,6 +69,8 @@ public partial class ChatWindow : Window
private WorkflowAnalyzerWindow? _analyzerWindow; // 워크플로우 분석기
private PlanViewerWindow? _planViewerWindow; // 실행 계획 뷰어
private Border? _userAskCard; // transcript 내 질문 카드
private string? _pendingPlanSummary;
private List<string> _pendingPlanSteps = new();
private bool _userScrolled; // 사용자가 위로 스크롤했는지
private readonly HashSet<string> _sessionPermissionRules = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, bool> _sessionMcpEnabledOverrides = new(StringComparer.OrdinalIgnoreCase);
@@ -8965,89 +8967,6 @@ public partial class ChatWindow : Window
}
}
private void UpdateTaskSummaryIndicators()
{
var hasLiveRuntimeActivity = !string.Equals(_activeTab, "Chat", StringComparison.OrdinalIgnoreCase)
&& (_runningConversationCount > 0 || _appState.ActiveTasks.Count > 0);
var status = _appState.GetOperationalStatusPresentation(_activeTab, hasLiveRuntimeActivity);
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)
{
if (!status.ShowCompactStrip)
{
ConversationStatusStrip.Visibility = Visibility.Collapsed;
ConversationStatusStripLabel.Text = "";
}
else if (string.Equals(status.StripKind, "permission_waiting", StringComparison.OrdinalIgnoreCase))
{
ConversationStatusStrip.Visibility = Visibility.Visible;
ConversationStatusStrip.Background = BrushFromHex("#FFF7ED");
ConversationStatusStrip.BorderBrush = BrushFromHex("#FDBA74");
ConversationStatusStripLabel.Foreground = BrushFromHex("#C2410C");
ConversationStatusStripLabel.Text = status.StripText;
}
else if (string.Equals(status.StripKind, "failed_run", StringComparison.OrdinalIgnoreCase)
|| string.Equals(status.StripKind, "permission_denied", StringComparison.OrdinalIgnoreCase))
{
ConversationStatusStrip.Visibility = Visibility.Visible;
ConversationStatusStrip.Background = BrushFromHex("#FEF2F2");
ConversationStatusStrip.BorderBrush = BrushFromHex("#FECACA");
ConversationStatusStripLabel.Foreground = BrushFromHex("#991B1B");
ConversationStatusStripLabel.Text = status.StripText;
}
else
{
ConversationStatusStrip.Visibility = Visibility.Collapsed;
ConversationStatusStripLabel.Text = "";
}
}
UpdateConversationQuickStripUi();
}
private void UpdateConversationQuickStripUi()
{
if (ConversationQuickStrip == null || QuickRunningLabel == null || QuickHotLabel == null
|| BtnQuickRunningFilter == null || BtnQuickHotSort == null)
return;
var allowQuickStrip = !string.Equals(_activeTab, "Chat", StringComparison.OrdinalIgnoreCase);
var hasQuickSignal = allowQuickStrip
&& ((_runningOnlyFilter && _runningConversationCount > 0)
|| (!_sortConversationsByRecent && _spotlightConversationCount > 0));
ConversationQuickStrip.Visibility = hasQuickSignal
? Visibility.Visible
: Visibility.Collapsed;
QuickRunningLabel.Text = _runningConversationCount > 0 ? $"진행 {_runningConversationCount}" : "진행";
QuickHotLabel.Text = _spotlightConversationCount > 0 ? $"활동 {_spotlightConversationCount}" : "활동";
BtnQuickRunningFilter.Background = _runningOnlyFilter ? BrushFromHex("#DBEAFE") : BrushFromHex("#F8FAFC");
BtnQuickRunningFilter.BorderBrush = _runningOnlyFilter ? BrushFromHex("#93C5FD") : BrushFromHex("#E5E7EB");
BtnQuickRunningFilter.BorderThickness = new Thickness(1);
QuickRunningLabel.Foreground = _runningOnlyFilter ? BrushFromHex("#1D4ED8") : (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray);
BtnQuickHotSort.Background = !_sortConversationsByRecent ? BrushFromHex("#F5F3FF") : BrushFromHex("#F8FAFC");
BtnQuickHotSort.BorderBrush = !_sortConversationsByRecent ? BrushFromHex("#C4B5FD") : BrushFromHex("#E5E7EB");
BtnQuickHotSort.BorderThickness = new Thickness(1);
QuickHotLabel.Foreground = !_sortConversationsByRecent ? BrushFromHex("#6D28D9") : (TryFindResource("SecondaryText") as Brush ?? Brushes.Gray);
}
private static string GetRunStatusLabel(string? status)
=> status switch
{
@@ -17812,53 +17731,6 @@ public partial class ChatWindow : Window
}
}
private void SetStatus(string text, bool spinning)
{
if (StatusLabel != null) StatusLabel.Text = text;
if (spinning) StartStatusAnimation();
}
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 StartStatusAnimation()
{
if (_statusSpinStoryboard != null) return;
@@ -17884,37 +17756,6 @@ public partial class ChatWindow : Window
_statusSpinStoryboard = null;
}
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 BtnCompactNow_Click(object sender, RoutedEventArgs e)
{
if (_isStreaming)