- 계획 승인 기본 흐름을 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:
@@ -7,6 +7,10 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
|
||||
개발 참고: Claw Code 동등성 작업 추적 문서
|
||||
`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)
|
||||
- 채팅/코워크 프리셋 카드의 hover 설명 레이어를 카드 내부 오버레이 방식에서 안정적인 tooltip형 설명으로 바꿨습니다. 카드 배경/테두리만 반응하게 정리해 hover 시 반복 깜빡임을 줄였습니다.
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# 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) - 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.
|
||||
|
||||
|
||||
@@ -167,6 +167,20 @@ public sealed class AppStateService
|
||||
public bool ShowCompactStrip { get; init; }
|
||||
public string StripKind { get; init; } = "none";
|
||||
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; }
|
||||
@@ -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 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, "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
|
||||
{
|
||||
ShowRuntimeBadge = status.ShowRuntimeBadge && hasLiveRuntimeActivity,
|
||||
@@ -648,6 +693,20 @@ public sealed class AppStateService
|
||||
ShowCompactStrip = showCompactStrip,
|
||||
StripKind = showCompactStrip ? status.StripKind : "none",
|
||||
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",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
@@ -259,6 +260,8 @@ public partial class ChatWindow
|
||||
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_pendingPlanSummary = planSummary;
|
||||
_pendingPlanSteps = steps.ToList();
|
||||
EnsurePlanViewerWindow();
|
||||
_planViewerWindow?.LoadPlan(planSummary, steps, tcs);
|
||||
ShowPlanButton(true);
|
||||
@@ -268,7 +271,11 @@ public partial class ChatWindow
|
||||
var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5)));
|
||||
if (completed != tcs.Task)
|
||||
{
|
||||
await Dispatcher.InvokeAsync(() => _planViewerWindow?.Hide());
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_planViewerWindow?.Hide();
|
||||
ResetPendingPlanPresentation();
|
||||
});
|
||||
return "취소";
|
||||
}
|
||||
|
||||
@@ -290,12 +297,15 @@ public partial class ChatWindow
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_planViewerWindow?.SwitchToExecutionMode();
|
||||
_planViewerWindow?.Hide();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await Dispatcher.InvokeAsync(() => _planViewerWindow?.Hide());
|
||||
await Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_planViewerWindow?.Hide();
|
||||
ResetPendingPlanPresentation();
|
||||
});
|
||||
}
|
||||
|
||||
return agentDecision;
|
||||
@@ -355,8 +365,18 @@ public partial class ChatWindow
|
||||
planBtn.MouseLeftButtonUp += (_, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
if (string.IsNullOrWhiteSpace(_pendingPlanSummary) && _pendingPlanSteps.Count == 0)
|
||||
return;
|
||||
|
||||
EnsurePlanViewerWindow();
|
||||
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.Activate();
|
||||
}
|
||||
@@ -377,6 +397,13 @@ public partial class ChatWindow
|
||||
{
|
||||
if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow))
|
||||
_planViewerWindow.MarkComplete();
|
||||
ResetPendingPlanPresentation();
|
||||
}
|
||||
|
||||
private void ResetPendingPlanPresentation()
|
||||
{
|
||||
_pendingPlanSummary = null;
|
||||
_pendingPlanSteps.Clear();
|
||||
ShowPlanButton(false);
|
||||
}
|
||||
|
||||
|
||||
172
src/AxCopilot/Views/ChatWindow.StatusPresentation.cs
Normal file
172
src/AxCopilot/Views/ChatWindow.StatusPresentation.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -306,6 +306,25 @@ internal sealed class PlanViewerWindow : Window
|
||||
_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)
|
||||
{
|
||||
LoadPlan(planText, steps, tcs);
|
||||
|
||||
Reference in New Issue
Block a user