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

@@ -7,6 +7,10 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
개발 참고: Claw Code 동등성 작업 추적 문서 개발 참고: Claw Code 동등성 작업 추적 문서
`docs/claw-code-parity-plan.md` `docs/claw-code-parity-plan.md`
- 업데이트: 2026-04-06 01:37 (KST)
- AX Agent의 계획 승인 흐름을 더 transcript 우선 구조로 정리했습니다. 인라인 승인 카드가 기본 경로를 맡고, `계획` 버튼은 저장된 계획 요약/단계를 여는 상세 보기 역할만 하도록 분리했습니다.
- 상태선과 quick strip 계산도 presentation state로 더 모았습니다. runtime badge, compact strip, quick strip의 텍스트/강조색/노출 여부를 한 번에 계산해 창 코드의 직접 분기를 줄였습니다.
- 업데이트: 2026-04-06 00:50 (KST) - 업데이트: 2026-04-06 00:50 (KST)
- 채팅/코워크 프리셋 카드의 hover 설명 레이어를 카드 내부 오버레이 방식에서 안정적인 tooltip형 설명으로 바꿨습니다. 카드 배경/테두리만 반응하게 정리해 hover 시 반복 깜빡임을 줄였습니다. - 채팅/코워크 프리셋 카드의 hover 설명 레이어를 카드 내부 오버레이 방식에서 안정적인 tooltip형 설명으로 바꿨습니다. 카드 배경/테두리만 반응하게 정리해 hover 시 반복 깜빡임을 줄였습니다.

View File

@@ -1,5 +1,8 @@
# AX Copilot - 媛쒕컻 臾몄꽌 # AX Copilot - 媛쒕컻 臾몄꽌
- Document update: 2026-04-06 01:37 (KST) - Reworked AX Agent plan approval toward a more transcript-native flow. The inline decision card remains the primary approval path, while the `계획` affordance now opens the stored plan as a detail-only surface instead of acting like a required popup step.
- Document update: 2026-04-06 01:37 (KST) - Expanded `OperationalStatusPresentationState` so runtime badge, compact strip, and quick-strip labels/colors/visibility are calculated together. `ChatWindow` now consumes a richer presentation model instead of branching on strip kinds and quick-strip counters independently.
- Document update: 2026-04-05 19:04 (KST) - Added transcript-facing tool/skill display normalization in `ChatWindow.xaml.cs`. Tool and skill events now use role-first badges (`도구`, `도구 결과`, `스킬`) with human-readable item labels such as `파일 읽기`, `빌드/실행`, `Git`, or `/skill-name`, instead of exposing raw snake_case names as the primary visual label. - Document update: 2026-04-05 19:04 (KST) - Added transcript-facing tool/skill display normalization in `ChatWindow.xaml.cs`. Tool and skill events now use role-first badges (`도구`, `도구 결과`, `스킬`) with human-readable item labels such as `파일 읽기`, `빌드/실행`, `Git`, or `/skill-name`, instead of exposing raw snake_case names as the primary visual label.
- Document update: 2026-04-05 19:04 (KST) - Reduced task-summary observability noise by limiting permission/background observability sections to debug-level sessions. Default AX Agent runtime UX now stays closer to `claw-code`, where transcript reading flow remains primary and diagnostics stay secondary. - Document update: 2026-04-05 19:04 (KST) - Reduced task-summary observability noise by limiting permission/background observability sections to debug-level sessions. Default AX Agent runtime UX now stays closer to `claw-code`, where transcript reading flow remains primary and diagnostics stay secondary.

View File

@@ -167,6 +167,20 @@ public sealed class AppStateService
public bool ShowCompactStrip { get; init; } public bool ShowCompactStrip { get; init; }
public string StripKind { get; init; } = "none"; public string StripKind { get; init; } = "none";
public string StripText { get; init; } = ""; public string StripText { get; init; } = "";
public string StripBackgroundHex { get; init; } = "";
public string StripBorderHex { get; init; } = "";
public string StripForegroundHex { get; init; } = "";
public bool ShowQuickStrip { get; init; }
public string QuickRunningText { get; init; } = "";
public string QuickHotText { get; init; } = "";
public bool QuickRunningActive { get; init; }
public bool QuickHotActive { get; init; }
public string QuickRunningBackgroundHex { get; init; } = "#F8FAFC";
public string QuickRunningBorderHex { get; init; } = "#E5E7EB";
public string QuickRunningForegroundHex { get; init; } = "#6B7280";
public string QuickHotBackgroundHex { get; init; } = "#F8FAFC";
public string QuickHotBorderHex { get; init; } = "#E5E7EB";
public string QuickHotForegroundHex { get; init; } = "#6B7280";
} }
public ChatSessionStateService? ChatSession { get; private set; } public ChatSessionStateService? ChatSession { get; private set; }
@@ -631,7 +645,13 @@ public sealed class AppStateService
}; };
} }
public OperationalStatusPresentationState GetOperationalStatusPresentation(string tab, bool hasLiveRuntimeActivity) public OperationalStatusPresentationState GetOperationalStatusPresentation(
string tab,
bool hasLiveRuntimeActivity,
int runningConversationCount,
int spotlightConversationCount,
bool runningOnlyFilter,
bool sortConversationsByRecent)
{ {
var status = GetOperationalStatus(tab); var status = GetOperationalStatus(tab);
var showCompactStrip = !string.Equals(tab, "Chat", StringComparison.OrdinalIgnoreCase) var showCompactStrip = !string.Equals(tab, "Chat", StringComparison.OrdinalIgnoreCase)
@@ -639,6 +659,31 @@ public sealed class AppStateService
|| string.Equals(status.StripKind, "failed_run", StringComparison.OrdinalIgnoreCase) || string.Equals(status.StripKind, "failed_run", StringComparison.OrdinalIgnoreCase)
|| string.Equals(status.StripKind, "permission_denied", StringComparison.OrdinalIgnoreCase)); || string.Equals(status.StripKind, "permission_denied", StringComparison.OrdinalIgnoreCase));
var stripBackgroundHex = "";
var stripBorderHex = "";
var stripForegroundHex = "";
if (showCompactStrip)
{
if (string.Equals(status.StripKind, "permission_waiting", StringComparison.OrdinalIgnoreCase))
{
stripBackgroundHex = "#FFF7ED";
stripBorderHex = "#FDBA74";
stripForegroundHex = "#C2410C";
}
else if (string.Equals(status.StripKind, "failed_run", StringComparison.OrdinalIgnoreCase)
|| string.Equals(status.StripKind, "permission_denied", StringComparison.OrdinalIgnoreCase))
{
stripBackgroundHex = "#FEF2F2";
stripBorderHex = "#FECACA";
stripForegroundHex = "#991B1B";
}
}
var allowQuickStrip = !string.Equals(tab, "Chat", StringComparison.OrdinalIgnoreCase);
var quickRunningActive = runningOnlyFilter && runningConversationCount > 0;
var quickHotActive = !sortConversationsByRecent && spotlightConversationCount > 0;
var showQuickStrip = allowQuickStrip && (quickRunningActive || quickHotActive);
return new OperationalStatusPresentationState return new OperationalStatusPresentationState
{ {
ShowRuntimeBadge = status.ShowRuntimeBadge && hasLiveRuntimeActivity, ShowRuntimeBadge = status.ShowRuntimeBadge && hasLiveRuntimeActivity,
@@ -648,6 +693,20 @@ public sealed class AppStateService
ShowCompactStrip = showCompactStrip, ShowCompactStrip = showCompactStrip,
StripKind = showCompactStrip ? status.StripKind : "none", StripKind = showCompactStrip ? status.StripKind : "none",
StripText = showCompactStrip ? status.StripText : "", StripText = showCompactStrip ? status.StripText : "",
StripBackgroundHex = stripBackgroundHex,
StripBorderHex = stripBorderHex,
StripForegroundHex = stripForegroundHex,
ShowQuickStrip = showQuickStrip,
QuickRunningText = runningConversationCount > 0 ? $"진행 {runningConversationCount}" : "진행",
QuickHotText = spotlightConversationCount > 0 ? $"활동 {spotlightConversationCount}" : "활동",
QuickRunningActive = quickRunningActive,
QuickHotActive = quickHotActive,
QuickRunningBackgroundHex = quickRunningActive ? "#DBEAFE" : "#F8FAFC",
QuickRunningBorderHex = quickRunningActive ? "#93C5FD" : "#E5E7EB",
QuickRunningForegroundHex = quickRunningActive ? "#1D4ED8" : "#6B7280",
QuickHotBackgroundHex = quickHotActive ? "#F5F3FF" : "#F8FAFC",
QuickHotBorderHex = quickHotActive ? "#C4B5FD" : "#E5E7EB",
QuickHotForegroundHex = quickHotActive ? "#6D28D9" : "#6B7280",
}; };
} }

View File

@@ -1,3 +1,4 @@
using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
@@ -259,6 +260,8 @@ public partial class ChatWindow
await Dispatcher.InvokeAsync(() => await Dispatcher.InvokeAsync(() =>
{ {
_pendingPlanSummary = planSummary;
_pendingPlanSteps = steps.ToList();
EnsurePlanViewerWindow(); EnsurePlanViewerWindow();
_planViewerWindow?.LoadPlan(planSummary, steps, tcs); _planViewerWindow?.LoadPlan(planSummary, steps, tcs);
ShowPlanButton(true); ShowPlanButton(true);
@@ -268,7 +271,11 @@ public partial class ChatWindow
var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5))); var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5)));
if (completed != tcs.Task) if (completed != tcs.Task)
{ {
await Dispatcher.InvokeAsync(() => _planViewerWindow?.Hide()); await Dispatcher.InvokeAsync(() =>
{
_planViewerWindow?.Hide();
ResetPendingPlanPresentation();
});
return "취소"; return "취소";
} }
@@ -290,12 +297,15 @@ public partial class ChatWindow
await Dispatcher.InvokeAsync(() => await Dispatcher.InvokeAsync(() =>
{ {
_planViewerWindow?.SwitchToExecutionMode(); _planViewerWindow?.SwitchToExecutionMode();
_planViewerWindow?.Hide();
}); });
} }
else else
{ {
await Dispatcher.InvokeAsync(() => _planViewerWindow?.Hide()); await Dispatcher.InvokeAsync(() =>
{
_planViewerWindow?.Hide();
ResetPendingPlanPresentation();
});
} }
return agentDecision; return agentDecision;
@@ -355,8 +365,18 @@ public partial class ChatWindow
planBtn.MouseLeftButtonUp += (_, e) => planBtn.MouseLeftButtonUp += (_, e) =>
{ {
e.Handled = true; e.Handled = true;
if (string.IsNullOrWhiteSpace(_pendingPlanSummary) && _pendingPlanSteps.Count == 0)
return;
EnsurePlanViewerWindow();
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow)) if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
{ {
if (string.IsNullOrWhiteSpace(_planViewerWindow.PlanText)
|| _planViewerWindow.PlanText != (_pendingPlanSummary ?? string.Empty)
|| !_planViewerWindow.Steps.SequenceEqual(_pendingPlanSteps))
{
_planViewerWindow.LoadPlanPreview(_pendingPlanSummary ?? "", _pendingPlanSteps);
}
_planViewerWindow.Show(); _planViewerWindow.Show();
_planViewerWindow.Activate(); _planViewerWindow.Activate();
} }
@@ -377,6 +397,13 @@ public partial class ChatWindow
{ {
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow)) if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
_planViewerWindow.MarkComplete(); _planViewerWindow.MarkComplete();
ResetPendingPlanPresentation();
}
private void ResetPendingPlanPresentation()
{
_pendingPlanSummary = null;
_pendingPlanSteps.Clear();
ShowPlanButton(false); ShowPlanButton(false);
} }

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();
}
}

View File

@@ -69,6 +69,8 @@ public partial class ChatWindow : Window
private WorkflowAnalyzerWindow? _analyzerWindow; // 워크플로우 분석기 private WorkflowAnalyzerWindow? _analyzerWindow; // 워크플로우 분석기
private PlanViewerWindow? _planViewerWindow; // 실행 계획 뷰어 private PlanViewerWindow? _planViewerWindow; // 실행 계획 뷰어
private Border? _userAskCard; // transcript 내 질문 카드 private Border? _userAskCard; // transcript 내 질문 카드
private string? _pendingPlanSummary;
private List<string> _pendingPlanSteps = new();
private bool _userScrolled; // 사용자가 위로 스크롤했는지 private bool _userScrolled; // 사용자가 위로 스크롤했는지
private readonly HashSet<string> _sessionPermissionRules = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet<string> _sessionPermissionRules = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, bool> _sessionMcpEnabledOverrides = 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) private static string GetRunStatusLabel(string? status)
=> status switch => 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() private void StartStatusAnimation()
{ {
if (_statusSpinStoryboard != null) return; if (_statusSpinStoryboard != null) return;
@@ -17884,37 +17756,6 @@ public partial class ChatWindow : Window
_statusSpinStoryboard = null; _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) private void BtnCompactNow_Click(object sender, RoutedEventArgs e)
{ {
if (_isStreaming) if (_isStreaming)

View File

@@ -306,6 +306,25 @@ internal sealed class PlanViewerWindow : Window
_statusBar.Visibility = Visibility.Collapsed; _statusBar.Visibility = Visibility.Collapsed;
} }
public void LoadPlanPreview(string planText, List<string> steps)
{
_planText = planText;
_steps = steps;
_tcs = null;
_currentStep = -1;
_isExecuting = false;
_expandedSteps.Clear();
if (_uiExpressionLevel == "rich")
{
for (int i = 0; i < _steps.Count; i++)
_expandedSteps.Add(i);
}
RenderSteps();
BuildCloseButton();
_statusBar.Visibility = Visibility.Collapsed;
}
public Task<string?> ShowPlanAsync(string planText, List<string> steps, TaskCompletionSource<string?> tcs) public Task<string?> ShowPlanAsync(string planText, List<string> steps, TaskCompletionSource<string?> tcs)
{ {
LoadPlan(planText, steps, tcs); LoadPlan(planText, steps, tcs);