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

@@ -1371,3 +1371,6 @@ MIT License
- 업데이트: 2026-04-06 22:31 (KST)
- AX Agent의 중간 처리 메시지 형식을 `claw-code`에 더 가깝게 재정리했습니다. `Thinking / ToolCall / StepStart / Planning` 계열은 작은 상태칩보다 `요약줄 + 본문 설명` 구조로 보이게 바꿔, 장시간 작업 중 “무슨 작업을 하고 있는지”를 일반 메시지처럼 읽을 수 있게 했습니다.
- 진행 메시지의 요약줄은 좌측 chevron과 보조 텍스트 중심으로 정리하고, 상세 설명은 바로 아래 본문 텍스트로 분리해 `claw-code / Claude Code`의 처리 메시지 밀도와 감각에 더 가깝게 맞췄습니다.
- 업데이트: 2026-04-06 22:42 (KST)
- AX Agent의 라이브 대기/압축 진행 힌트를 `claw-code`처럼 더 읽기 쉬운 진행 한 줄로 보강했습니다. 이제 Cowork/Code에서 오래 걸릴 때 `처리 중...`, `컨텍스트 압축 중...` 같은 요약줄이 transcript에 살아 있는 진행 상태로 나타납니다.
- 같은 진행 줄 우측에는 `경과 시간``현재 누적 토큰`이 함께 표시되어, 사용자가 “지금 멈춘 건지 아직 처리 중인지”를 기다릴 근거와 함께 바로 확인할 수 있게 조정했습니다.

View File

@@ -5066,3 +5066,12 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
- 기존의 작은 pill 카드 대신 `chevron 요약줄 + 본문 설명` 구조로 렌더
- 장시간 작업 중에도 단계/툴 실행/생각 중 상태가 일반 assistant 진행 메시지처럼 읽히도록 정리
- permission/result 카드와 진행 중 feed를 분리해, 실제 처리 메시지는 `claude-code / Claude Code`처럼 더 담백한 transcript 흐름으로 보이게 조정했다.
## 2026-04-06 22:42 (KST)
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 라이브 진행 힌트 이벤트에 `ElapsedMs`, `InputTokens`, `OutputTokens`를 실시간으로 채워 넣도록 확장했다.
- Cowork/Code에서 스트리밍 중 오래 걸릴 때마다 현재 경과 시간과 누적 토큰 수가 진행 힌트 자체에 반영된다.
- 대기/압축 힌트는 초 단위 경과 시간이나 토큰 수가 바뀌면 자동 갱신되도록 비교 조건도 함께 보강했다.
- [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs)의 process feed 요약줄에 우측 메타 영역을 추가했다.
- `처리 중...`, `컨텍스트 압축 중...` 같은 라이브 진행 줄에 `6m 46s · 38.8K tokens`처럼 기다릴 근거가 함께 노출된다.
- 라이브 대기/압축 상태는 기존 chevron 대신 강조 마커와 조금 더 진한 제목 톤을 써서 일반 이벤트보다 눈에 잘 들어오도록 구분했다.

View File

@@ -10,7 +10,15 @@ namespace AxCopilot.Views;
public partial class ChatWindow
{
private Border CreateCompactEventPill(string summary, Brush primaryText, Brush secondaryText, Brush hintBg, Brush borderBrush, Brush accentBrush)
private Border CreateCompactEventPill(
string summary,
Brush primaryText,
Brush secondaryText,
Brush hintBg,
Brush borderBrush,
Brush accentBrush,
string? metaText = null,
bool liveWaitingStyle = false)
{
return new Border
{
@@ -21,27 +29,49 @@ public partial class ChatWindow
Padding = new Thickness(0),
Margin = new Thickness(12, 4, 12, 2),
HorizontalAlignment = HorizontalAlignment.Left,
Child = new StackPanel
Child = new Grid
{
Orientation = Orientation.Horizontal,
ColumnDefinitions =
{
new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) },
new ColumnDefinition { Width = GridLength.Auto },
},
Children =
{
new TextBlock
new StackPanel
{
Text = "",
FontSize = 12,
Foreground = secondaryText,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 6, 0),
Orientation = Orientation.Horizontal,
Children =
{
new TextBlock
{
Text = liveWaitingStyle ? "✶" : "",
FontSize = liveWaitingStyle ? 11.5 : 12,
Foreground = liveWaitingStyle ? accentBrush : secondaryText,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 6, 0),
FontWeight = liveWaitingStyle ? FontWeights.SemiBold : FontWeights.Normal,
},
new TextBlock
{
Text = summary,
FontSize = liveWaitingStyle ? 11.5 : 11,
FontWeight = liveWaitingStyle ? FontWeights.SemiBold : FontWeights.Normal,
Foreground = liveWaitingStyle ? primaryText : secondaryText,
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.Wrap,
}
}
},
new TextBlock
{
Text = summary,
FontSize = 11,
FontWeight = FontWeights.Normal,
Text = metaText ?? "",
FontSize = 10,
FontWeight = liveWaitingStyle ? FontWeights.Medium : FontWeights.Normal,
Foreground = secondaryText,
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(10, 0, 0, 0),
Visibility = string.IsNullOrWhiteSpace(metaText) ? Visibility.Collapsed : Visibility.Visible,
}
}
}
@@ -82,6 +112,10 @@ public partial class ChatWindow
{
return evt.Type switch
{
AgentEventType.Thinking when string.Equals(evt.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase)
=> "처리 중...",
AgentEventType.Thinking when string.Equals(evt.ToolName, "context_compaction", StringComparison.OrdinalIgnoreCase)
=> "컨텍스트 압축 중...",
AgentEventType.Planning when evt.Steps is { Count: > 0 }
=> $"{evt.Steps.Count}단계 계획 정리",
AgentEventType.StepStart when evt.StepTotal > 0
@@ -111,6 +145,7 @@ public partial class ChatWindow
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
var processMeta = BuildProcessFeedMetaText(evt);
var summary = BuildProcessFeedSummary(evt, transcriptBadgeLabel, itemDisplayName).Trim();
if (string.IsNullOrWhiteSpace(summary))
summary = transcriptBadgeLabel;
@@ -120,7 +155,18 @@ public partial class ChatWindow
Margin = new Thickness(0),
};
var summaryRow = CreateCompactEventPill(summary, primaryText, secondaryText, hintBg, borderBrush, accentBrush);
var liveWaitingStyle = evt.Type == AgentEventType.Thinking
&& (string.Equals(evt.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase)
|| string.Equals(evt.ToolName, "context_compaction", StringComparison.OrdinalIgnoreCase));
var summaryRow = CreateCompactEventPill(
summary,
primaryText,
secondaryText,
hintBg,
borderBrush,
accentBrush,
processMeta,
liveWaitingStyle);
summaryRow.Opacity = 0;
summaryRow.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(140)));
stack.Children.Add(summaryRow);
@@ -166,6 +212,28 @@ public partial class ChatWindow
MessagePanel.Children.Add(stack);
}
private string BuildProcessFeedMetaText(AgentEvent evt)
{
var parts = new List<string>();
if (evt.ElapsedMs > 0)
{
var elapsed = TimeSpan.FromMilliseconds(evt.ElapsedMs);
if (elapsed.TotalHours >= 1)
parts.Add($"{(int)elapsed.TotalHours}h {elapsed.Minutes}m");
else if (elapsed.TotalMinutes >= 1)
parts.Add($"{(int)elapsed.TotalMinutes}m {elapsed.Seconds}s");
else
parts.Add($"{Math.Max(1, elapsed.Seconds)}s");
}
var totalTokens = Math.Max(0, evt.InputTokens) + Math.Max(0, evt.OutputTokens);
if (totalTokens > 0)
parts.Add($"{FormatTokenCount(totalTokens)} tokens");
return string.Join(" · ", parts);
}
private Border CreateAgentMetaChip(string text, string icon, Brush foreground, Brush background, Brush borderBrush)
{
return new Border

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(() =>