AX Agent 라이브 진행 메시지에 경과 시간·토큰 표시 추가\n\n- Cowork/Code 장기 대기와 컨텍스트 압축 상황을 transcript에서 더 명확히 보이도록 라이브 진행 힌트 이벤트를 확장\n- 라이브 진행 줄 우측에 경과 시간과 누적 토큰 수를 표시해 사용자가 멈춤과 처리 중 상태를 구분할 수 있게 조정\n- process feed 요약줄 스타일을 보강해 claw-code 레퍼런스처럼 대기/압축 상태가 더 눈에 잘 들어오도록 개선\n- README 및 DEVELOPMENT 문서에 2026-04-06 22:42 (KST) 기준 변경 이력 반영\n\n검증 결과\n- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\\n- 경고 0개, 오류 0개

This commit is contained in:
2026-04-06 22:44:24 +09:00
parent 4992dca74f
commit 306245524d
4 changed files with 223 additions and 14 deletions

View File

@@ -92,6 +92,7 @@ public partial class ChatWindow : Window
private readonly DispatcherTimer _taskSummaryRefreshTimer;
private readonly DispatcherTimer _conversationPersistTimer;
private readonly DispatcherTimer _agentUiEventTimer;
private readonly DispatcherTimer _agentProgressHintTimer;
private readonly DispatcherTimer _tokenUsagePopupCloseTimer;
private readonly DispatcherTimer _responsiveLayoutTimer;
private CancellationTokenSource? _gitStatusRefreshCts;
@@ -131,6 +132,8 @@ public partial class ChatWindow : Window
private readonly Dictionary<string, ChatConversation> _pendingConversationPersists = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _expandedDraftQueueTabs = new(StringComparer.OrdinalIgnoreCase);
private AgentEvent? _pendingAgentUiEvent;
private AgentEvent? _liveAgentProgressHint;
private DateTime _lastAgentProgressEventAt = DateTime.UtcNow;
private double _lastResponsiveComposerWidth;
private double _lastResponsiveMessageWidth;
private void ApplyQuickActionVisual(Button button, bool active, string activeBg, string activeFg)
@@ -268,6 +271,8 @@ public partial class ChatWindow : Window
_agentUiEventTimer.Stop();
FlushPendingAgentUiEvent();
};
_agentProgressHintTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_agentProgressHintTimer.Tick += AgentProgressHintTimer_Tick;
_tokenUsagePopupCloseTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(140) };
_tokenUsagePopupCloseTimer.Tick += (_, _) =>
{
@@ -5286,6 +5291,7 @@ public partial class ChatWindow : Window
_isStreaming = true;
_streamRunTab = runTab;
StartLiveAgentProgressHints();
BtnSend.IsEnabled = false;
BtnSend.Visibility = Visibility.Collapsed;
BtnStop.Visibility = Visibility.Visible;
@@ -5468,6 +5474,7 @@ public partial class ChatWindow : Window
{
FlushPendingConversationPersists();
FlushPendingAgentUiEvent();
StopLiveAgentProgressHints();
_cursorTimer.Stop();
_elapsedTimer.Stop();
_typingTimer.Stop();
@@ -6135,6 +6142,7 @@ public partial class ChatWindow : Window
private void OnAgentEvent(AgentEvent evt)
{
TouchLiveAgentProgressHints();
var eventTab = string.IsNullOrWhiteSpace(_streamRunTab) ? _activeTab : _streamRunTab!;
// 실행 로그는 직접 배너를 먼저 꽂지 않고, 대화 모델에 누적한 뒤 재렌더합니다.
@@ -6164,6 +6172,127 @@ public partial class ChatWindow : Window
ScheduleTaskSummaryRefresh();
}
private void StartLiveAgentProgressHints()
{
_lastAgentProgressEventAt = DateTime.UtcNow;
UpdateLiveAgentProgressHint(null);
_agentProgressHintTimer.Stop();
_agentProgressHintTimer.Start();
}
private void StopLiveAgentProgressHints()
{
_agentProgressHintTimer.Stop();
UpdateLiveAgentProgressHint(null);
}
private void TouchLiveAgentProgressHints()
{
_lastAgentProgressEventAt = DateTime.UtcNow;
UpdateLiveAgentProgressHint(null);
}
private void AgentProgressHintTimer_Tick(object? sender, EventArgs e)
{
if (!_isStreaming || !_agentLoop.IsRunning)
{
StopLiveAgentProgressHints();
return;
}
var runTab = string.IsNullOrWhiteSpace(_streamRunTab) ? _activeTab : _streamRunTab!;
if (!string.Equals(runTab, "Cowork", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(runTab, "Code", StringComparison.OrdinalIgnoreCase))
{
UpdateLiveAgentProgressHint(null);
return;
}
var idle = DateTime.UtcNow - _lastAgentProgressEventAt;
string? summary = null;
var toolName = "agent_wait";
if (_pendingPostCompaction && idle >= TimeSpan.FromSeconds(2))
{
toolName = "context_compaction";
summary = idle >= TimeSpan.FromSeconds(12)
? "컨텍스트를 압축한 뒤 응답을 정리하는 중입니다..."
: "컨텍스트를 압축하고 있습니다...";
}
else if (idle >= TimeSpan.FromSeconds(12))
{
summary = "응답을 정리하는 중입니다. 아직 처리 중입니다...";
}
else if (idle >= TimeSpan.FromSeconds(5))
{
summary = "생각을 정리하는 중입니다...";
}
UpdateLiveAgentProgressHint(summary, toolName);
}
private void UpdateLiveAgentProgressHint(string? summary, string toolName = "")
{
var normalizedSummary = string.IsNullOrWhiteSpace(summary) ? null : summary.Trim();
var currentSummary = _liveAgentProgressHint?.Summary;
var currentToolName = _liveAgentProgressHint?.ToolName ?? "";
var elapsedMs = _isStreaming
? Math.Max(0L, (long)(DateTime.UtcNow - _streamStartTime.ToUniversalTime()).TotalMilliseconds)
: 0L;
var inputTokens = Math.Max(0, _agentCumulativeInputTokens);
var outputTokens = Math.Max(0, _agentCumulativeOutputTokens);
var currentElapsedBucket = (_liveAgentProgressHint?.ElapsedMs ?? 0) / 1000;
var nextElapsedBucket = elapsedMs / 1000;
if (string.Equals(currentSummary, normalizedSummary, StringComparison.Ordinal)
&& string.Equals(currentToolName, toolName, StringComparison.Ordinal)
&& currentElapsedBucket == nextElapsedBucket
&& (_liveAgentProgressHint?.InputTokens ?? 0) == inputTokens
&& (_liveAgentProgressHint?.OutputTokens ?? 0) == outputTokens)
return;
_liveAgentProgressHint = normalizedSummary == null
? null
: new AgentEvent
{
Timestamp = DateTime.Now,
RunId = _appState.AgentRun.RunId,
Type = AgentEventType.Thinking,
ToolName = toolName,
Summary = normalizedSummary,
ElapsedMs = elapsedMs,
InputTokens = inputTokens,
OutputTokens = outputTokens,
};
if (MessagePanel != null)
RenderMessages(preserveViewport: true);
}
private AgentEvent? GetLiveAgentProgressHint()
{
if (_liveAgentProgressHint == null)
return null;
return new AgentEvent
{
Timestamp = _liveAgentProgressHint.Timestamp,
RunId = _liveAgentProgressHint.RunId,
Type = _liveAgentProgressHint.Type,
ToolName = _liveAgentProgressHint.ToolName,
Summary = _liveAgentProgressHint.Summary,
FilePath = _liveAgentProgressHint.FilePath,
Success = _liveAgentProgressHint.Success,
StepCurrent = _liveAgentProgressHint.StepCurrent,
StepTotal = _liveAgentProgressHint.StepTotal,
Steps = _liveAgentProgressHint.Steps,
ElapsedMs = _liveAgentProgressHint.ElapsedMs,
InputTokens = _liveAgentProgressHint.InputTokens,
OutputTokens = _liveAgentProgressHint.OutputTokens,
ToolInput = _liveAgentProgressHint.ToolInput,
Iteration = _liveAgentProgressHint.Iteration,
};
}
private void OnSubAgentStatusChanged(SubAgentStatusEvent evt)
{
Dispatcher.Invoke(() =>