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:
@@ -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에 살아 있는 진행 상태로 나타납니다.
|
||||
- 같은 진행 줄 우측에는 `경과 시간`과 `현재 누적 토큰`이 함께 표시되어, 사용자가 “지금 멈춘 건지 아직 처리 중인지”를 기다릴 근거와 함께 바로 확인할 수 있게 조정했습니다.
|
||||
|
||||
@@ -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 대신 강조 마커와 조금 더 진한 제목 톤을 써서 일반 이벤트보다 눈에 잘 들어오도록 구분했다.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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