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

@@ -0,0 +1,172 @@
using System.Windows;
using System.Windows.Media;
using AxCopilot.Services;
namespace AxCopilot.Views;
public partial class ChatWindow
{
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();
}
}