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:
@@ -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(() =>
|
||||
|
||||
Reference in New Issue
Block a user