Compare commits

...

2 Commits

4 changed files with 272 additions and 14 deletions

View File

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

View File

@@ -5066,3 +5066,18 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
- 기존의 작은 pill 카드 대신 `chevron 요약줄 + 본문 설명` 구조로 렌더 - 기존의 작은 pill 카드 대신 `chevron 요약줄 + 본문 설명` 구조로 렌더
- 장시간 작업 중에도 단계/툴 실행/생각 중 상태가 일반 assistant 진행 메시지처럼 읽히도록 정리 - 장시간 작업 중에도 단계/툴 실행/생각 중 상태가 일반 assistant 진행 메시지처럼 읽히도록 정리
- permission/result 카드와 진행 중 feed를 분리해, 실제 처리 메시지는 `claude-code / Claude Code`처럼 더 담백한 transcript 흐름으로 보이게 조정했다. - 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 대신 강조 마커와 조금 더 진한 제목 톤을 써서 일반 이벤트보다 눈에 잘 들어오도록 구분했다.
## 2026-04-06 22:48 (KST)
- [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs)의 라이브 진행 줄에 작은 펄스 애니메이션을 추가했다.
- `처리 중...`, `컨텍스트 압축 중...`처럼 오래 걸리는 thinking 상태는 좌측 마커가 opacity/scale 펄스를 반복하며 살아 있는 상태임을 보여준다.
- transcript 분위기를 과하게 흔들지 않도록 마커 하나만 은은하게 움직이고, 본문 레이아웃은 그대로 유지해 `claude-code`의 담백한 진행감에 가깝게 맞췄다.

View File

@@ -10,7 +10,15 @@ namespace AxCopilot.Views;
public partial class ChatWindow 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 return new Border
{ {
@@ -21,29 +29,51 @@ public partial class ChatWindow
Padding = new Thickness(0), Padding = new Thickness(0),
Margin = new Thickness(12, 4, 12, 2), Margin = new Thickness(12, 4, 12, 2),
HorizontalAlignment = HorizontalAlignment.Left, HorizontalAlignment = HorizontalAlignment.Left,
Child = new StackPanel Child = new Grid
{
ColumnDefinitions =
{
new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) },
new ColumnDefinition { Width = GridLength.Auto },
},
Children =
{
new StackPanel
{ {
Orientation = Orientation.Horizontal, Orientation = Orientation.Horizontal,
Children = Children =
{ {
new TextBlock new TextBlock
{ {
Text = "", Text = liveWaitingStyle ? "✶" : "",
FontSize = 12, FontSize = liveWaitingStyle ? 11.5 : 12,
Foreground = secondaryText, Foreground = liveWaitingStyle ? accentBrush : secondaryText,
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 6, 0), Margin = new Thickness(0, 0, 6, 0),
FontWeight = liveWaitingStyle ? FontWeights.SemiBold : FontWeights.Normal,
}, },
new TextBlock new TextBlock
{ {
Text = summary, Text = summary,
FontSize = 11, FontSize = liveWaitingStyle ? 11.5 : 11,
FontWeight = FontWeights.Normal, FontWeight = liveWaitingStyle ? FontWeights.SemiBold : FontWeights.Normal,
Foreground = secondaryText, Foreground = liveWaitingStyle ? primaryText : secondaryText,
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.Wrap, TextWrapping = TextWrapping.Wrap,
} }
} }
},
new TextBlock
{
Text = metaText ?? "",
FontSize = 10,
FontWeight = liveWaitingStyle ? FontWeights.Medium : FontWeights.Normal,
Foreground = secondaryText,
VerticalAlignment = VerticalAlignment.Center,
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 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 } AgentEventType.Planning when evt.Steps is { Count: > 0 }
=> $"{evt.Steps.Count}단계 계획 정리", => $"{evt.Steps.Count}단계 계획 정리",
AgentEventType.StepStart when evt.StepTotal > 0 AgentEventType.StepStart when evt.StepTotal > 0
@@ -111,6 +145,7 @@ public partial class ChatWindow
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
var processMeta = BuildProcessFeedMetaText(evt);
var summary = BuildProcessFeedSummary(evt, transcriptBadgeLabel, itemDisplayName).Trim(); var summary = BuildProcessFeedSummary(evt, transcriptBadgeLabel, itemDisplayName).Trim();
if (string.IsNullOrWhiteSpace(summary)) if (string.IsNullOrWhiteSpace(summary))
summary = transcriptBadgeLabel; summary = transcriptBadgeLabel;
@@ -120,9 +155,22 @@ public partial class ChatWindow
Margin = new Thickness(0), 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.Opacity = 0;
summaryRow.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(140))); summaryRow.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(140)));
if (liveWaitingStyle)
ApplyLiveWaitingPulse(summaryRow);
stack.Children.Add(summaryRow); stack.Children.Add(summaryRow);
var body = (eventSummaryText ?? string.Empty).Trim(); var body = (eventSummaryText ?? string.Empty).Trim();
@@ -166,6 +214,67 @@ public partial class ChatWindow
MessagePanel.Children.Add(stack); MessagePanel.Children.Add(stack);
} }
private static void ApplyLiveWaitingPulse(Border summaryRow)
{
if (summaryRow.Child is not Grid grid || grid.Children.Count == 0 || grid.Children[0] is not StackPanel panel)
return;
if (panel.Children.Count == 0 || panel.Children[0] is not TextBlock marker)
return;
marker.Text = "✶";
marker.RenderTransformOrigin = new Point(0.5, 0.5);
marker.RenderTransform = new ScaleTransform(1, 1);
var pulse = new DoubleAnimation
{
From = 0.42,
To = 1.0,
Duration = TimeSpan.FromMilliseconds(780),
AutoReverse = true,
RepeatBehavior = RepeatBehavior.Forever,
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseInOut },
};
marker.BeginAnimation(UIElement.OpacityProperty, pulse);
if (marker.RenderTransform is ScaleTransform scale)
{
var scaleAnimX = new DoubleAnimation
{
From = 0.92,
To = 1.08,
Duration = TimeSpan.FromMilliseconds(780),
AutoReverse = true,
RepeatBehavior = RepeatBehavior.Forever,
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseInOut },
};
var scaleAnimY = scaleAnimX.Clone();
scale.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimX);
scale.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimY);
}
}
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) private Border CreateAgentMetaChip(string text, string icon, Brush foreground, Brush background, Brush borderBrush)
{ {
return new Border return new Border

View File

@@ -92,6 +92,7 @@ public partial class ChatWindow : Window
private readonly DispatcherTimer _taskSummaryRefreshTimer; private readonly DispatcherTimer _taskSummaryRefreshTimer;
private readonly DispatcherTimer _conversationPersistTimer; private readonly DispatcherTimer _conversationPersistTimer;
private readonly DispatcherTimer _agentUiEventTimer; private readonly DispatcherTimer _agentUiEventTimer;
private readonly DispatcherTimer _agentProgressHintTimer;
private readonly DispatcherTimer _tokenUsagePopupCloseTimer; private readonly DispatcherTimer _tokenUsagePopupCloseTimer;
private readonly DispatcherTimer _responsiveLayoutTimer; private readonly DispatcherTimer _responsiveLayoutTimer;
private CancellationTokenSource? _gitStatusRefreshCts; private CancellationTokenSource? _gitStatusRefreshCts;
@@ -131,6 +132,8 @@ public partial class ChatWindow : Window
private readonly Dictionary<string, ChatConversation> _pendingConversationPersists = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, ChatConversation> _pendingConversationPersists = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _expandedDraftQueueTabs = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet<string> _expandedDraftQueueTabs = new(StringComparer.OrdinalIgnoreCase);
private AgentEvent? _pendingAgentUiEvent; private AgentEvent? _pendingAgentUiEvent;
private AgentEvent? _liveAgentProgressHint;
private DateTime _lastAgentProgressEventAt = DateTime.UtcNow;
private double _lastResponsiveComposerWidth; private double _lastResponsiveComposerWidth;
private double _lastResponsiveMessageWidth; private double _lastResponsiveMessageWidth;
private void ApplyQuickActionVisual(Button button, bool active, string activeBg, string activeFg) private void ApplyQuickActionVisual(Button button, bool active, string activeBg, string activeFg)
@@ -268,6 +271,8 @@ public partial class ChatWindow : Window
_agentUiEventTimer.Stop(); _agentUiEventTimer.Stop();
FlushPendingAgentUiEvent(); FlushPendingAgentUiEvent();
}; };
_agentProgressHintTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_agentProgressHintTimer.Tick += AgentProgressHintTimer_Tick;
_tokenUsagePopupCloseTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(140) }; _tokenUsagePopupCloseTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(140) };
_tokenUsagePopupCloseTimer.Tick += (_, _) => _tokenUsagePopupCloseTimer.Tick += (_, _) =>
{ {
@@ -5286,6 +5291,7 @@ public partial class ChatWindow : Window
_isStreaming = true; _isStreaming = true;
_streamRunTab = runTab; _streamRunTab = runTab;
StartLiveAgentProgressHints();
BtnSend.IsEnabled = false; BtnSend.IsEnabled = false;
BtnSend.Visibility = Visibility.Collapsed; BtnSend.Visibility = Visibility.Collapsed;
BtnStop.Visibility = Visibility.Visible; BtnStop.Visibility = Visibility.Visible;
@@ -5468,6 +5474,7 @@ public partial class ChatWindow : Window
{ {
FlushPendingConversationPersists(); FlushPendingConversationPersists();
FlushPendingAgentUiEvent(); FlushPendingAgentUiEvent();
StopLiveAgentProgressHints();
_cursorTimer.Stop(); _cursorTimer.Stop();
_elapsedTimer.Stop(); _elapsedTimer.Stop();
_typingTimer.Stop(); _typingTimer.Stop();
@@ -6135,6 +6142,7 @@ public partial class ChatWindow : Window
private void OnAgentEvent(AgentEvent evt) private void OnAgentEvent(AgentEvent evt)
{ {
TouchLiveAgentProgressHints();
var eventTab = string.IsNullOrWhiteSpace(_streamRunTab) ? _activeTab : _streamRunTab!; var eventTab = string.IsNullOrWhiteSpace(_streamRunTab) ? _activeTab : _streamRunTab!;
// 실행 로그는 직접 배너를 먼저 꽂지 않고, 대화 모델에 누적한 뒤 재렌더합니다. // 실행 로그는 직접 배너를 먼저 꽂지 않고, 대화 모델에 누적한 뒤 재렌더합니다.
@@ -6164,6 +6172,127 @@ public partial class ChatWindow : Window
ScheduleTaskSummaryRefresh(); 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) private void OnSubAgentStatusChanged(SubAgentStatusEvent evt)
{ {
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>