- 계획 승인 기본 흐름을 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 동등성 작업 추적 문서
|
개발 참고: 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 시 반복 깜빡임을 줄였습니다.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user