에이전트 진행 표시 구조를 claude-code식 row 기반으로 재정리
Some checks failed
Release Gate / gate (push) Has been cancelled

- AgentTranscriptDisplayCatalog를 row presentation 중심으로 재구성해 thinking/waiting/compact/tool activity/permission/tool result/status를 타입별로 분리함
- PermissionRequestPresentationCatalog와 ToolResultPresentationCatalog를 정리해 권한 요청과 결과 상태를 행위/상태 기준으로 더 명확하게 표현함
- ChatWindow.AgentEventRendering에서 process feed 계열 이벤트를 GroupKey 기준으로 병합해 append 수를 줄이고 진행 흐름이 기본 transcript에 남도록 조정함
- FooterPresentation에서 Cowork/Chat 프리셋 안내 카드가 execution event 이후 자동으로 숨겨지도록 하고 입력 워터마크와 footer 기본 문구를 정리함
- render_messages 성능 로그에 processFeed append/merge 수치와 rowKindCounts를 추가해 %APPDATA%\\AxCopilot\\perf 기준 실검증이 가능하도록 함
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 기준 경고 0 / 오류 0 확인
This commit is contained in:
2026-04-09 14:49:53 +09:00
parent 33c1db4dae
commit 227f5ab0d3
9 changed files with 867 additions and 548 deletions

View File

@@ -11,6 +11,12 @@ namespace AxCopilot.Views;
public partial class ChatWindow
{
private string? _lastGroupedProcessFeedKey;
private int _lastGroupedProcessFeedIndex = -1;
private int _processFeedAppendCount;
private int _processFeedMergeCount;
private readonly Dictionary<TranscriptRowKind, int> _transcriptRowKindCounts = new();
private static Color ResolveLiveProgressAccentColor(Brush accentBrush)
{
return accentBrush is SolidColorBrush solid
@@ -164,8 +170,24 @@ public partial class ChatWindow
};
}
private void AddProcessFeedMessage(AgentEvent evt, string transcriptBadgeLabel, string itemDisplayName, string? eventSummaryText)
private void ResetProcessFeedGrouping()
{
_lastGroupedProcessFeedKey = null;
_lastGroupedProcessFeedIndex = -1;
}
private void TrackTranscriptRowKind(TranscriptRowKind kind)
{
if (_transcriptRowKindCounts.TryGetValue(kind, out var count))
_transcriptRowKindCounts[kind] = count + 1;
else
_transcriptRowKindCounts[kind] = 1;
}
private void AddProcessFeedMessage(AgentEvent evt, AgentTranscriptRowPresentation rowPresentation, string? eventSummaryText)
{
TrackTranscriptRowKind(rowPresentation.Kind);
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray;
var hintBg = TryFindResource("HintBackground") as Brush
@@ -174,9 +196,9 @@ public partial class ChatWindow
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
var processMeta = BuildReadableProgressMetaText(evt);
var summary = BuildReadableProcessFeedSummary(evt, transcriptBadgeLabel, itemDisplayName).Trim();
var summary = rowPresentation.Title.Trim();
if (string.IsNullOrWhiteSpace(summary))
summary = transcriptBadgeLabel;
summary = rowPresentation.BadgeLabel;
var msgMaxWidth = GetMessageMaxWidth();
var stack = new StackPanel
@@ -203,7 +225,7 @@ public partial class ChatWindow
ApplyLiveWaitingPulseToMarker(pulseMarker);
stack.Children.Add(summaryRow);
var body = (eventSummaryText ?? string.Empty).Trim();
var body = (string.IsNullOrWhiteSpace(eventSummaryText) ? rowPresentation.Description : eventSummaryText ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(body)
&& !string.Equals(body, summary, StringComparison.OrdinalIgnoreCase))
{
@@ -241,7 +263,21 @@ public partial class ChatWindow
stack.Children.Add(compactPathRow);
}
AddTranscriptElement(stack);
if (rowPresentation.CanGroup &&
!string.IsNullOrWhiteSpace(rowPresentation.GroupKey) &&
string.Equals(_lastGroupedProcessFeedKey, rowPresentation.GroupKey, StringComparison.Ordinal) &&
_lastGroupedProcessFeedIndex >= 0)
{
ReplaceTranscriptElement(_lastGroupedProcessFeedIndex, stack);
_processFeedMergeCount++;
}
else
{
AddTranscriptElement(stack);
_lastGroupedProcessFeedIndex = Math.Max(0, GetTranscriptElementCount() - 1);
_lastGroupedProcessFeedKey = rowPresentation.CanGroup ? rowPresentation.GroupKey : null;
_processFeedAppendCount++;
}
}
private static void ApplyLiveWaitingPulse(Border summaryRow)
@@ -1151,6 +1187,9 @@ public partial class ChatWindow
var toolResultPresentation = evt.Type == AgentEventType.ToolResult
? ToolResultPresentationCatalog.Resolve(evt, transcriptBadgeLabel)
: null;
var rowPresentation = AgentTranscriptDisplayCatalog.ResolveRowPresentation(evt, itemDisplayName: evt.Type == AgentEventType.SkillCall
? GetAgentItemDisplayName(evt.ToolName, slashPrefix: true)
: GetAgentItemDisplayName(evt.ToolName), transcriptBadgeLabel);
var (icon, label, bgHex, fgHex) = isTotalStats
? ("\uE9D2", "전체 통계", "#F3EEFF", "#7C3AED")
@@ -1179,11 +1218,39 @@ public partial class ChatWindow
{
eventSummaryText = evt.Type switch
{
AgentEventType.PermissionRequest or AgentEventType.PermissionGranted => permissionPresentation?.Description ?? "",
AgentEventType.ToolResult => toolResultPresentation?.Description ?? "",
_ => ""
AgentEventType.PermissionRequest or AgentEventType.PermissionGranted => permissionPresentation?.Description ?? rowPresentation.Description,
AgentEventType.ToolResult => toolResultPresentation?.Description ?? rowPresentation.Description,
_ => rowPresentation.Description
};
}
else if (string.IsNullOrWhiteSpace(rowPresentation.Description) == false &&
string.Equals(eventSummaryText, evt.Summary, StringComparison.OrdinalIgnoreCase))
{
eventSummaryText = rowPresentation.Description;
}
if (IsProcessFeedEvent(evt))
{
AddProcessFeedMessage(evt, rowPresentation, eventSummaryText);
return;
}
ResetProcessFeedGrouping();
TrackTranscriptRowKind(rowPresentation.Kind);
if (rowPresentation.Kind == TranscriptRowKind.ToolResult && toolResultPresentation != null)
{
eventSummaryText = rowPresentation.Description;
}
else if (rowPresentation.Kind == TranscriptRowKind.Permission && permissionPresentation != null)
{
eventSummaryText = rowPresentation.Description;
}
if (string.IsNullOrWhiteSpace(label) && !string.IsNullOrWhiteSpace(rowPresentation.BadgeLabel))
label = rowPresentation.BadgeLabel;
if (string.IsNullOrWhiteSpace(eventSummaryText))
eventSummaryText = rowPresentation.Description;
// HTML/대용량 파일 내용이 이벤트 요약에 포함된 경우 인라인 표시 대신 1줄 요약으로 축소
if (!string.IsNullOrWhiteSpace(eventSummaryText) && evt.Type == AgentEventType.ToolResult
@@ -1212,12 +1279,6 @@ public partial class ChatWindow
if (evt.Type == AgentEventType.StepStart && evt.StepTotal > 0)
UpdateProgressBar(evt);
if (IsProcessFeedEvent(evt))
{
AddProcessFeedMessage(evt, transcriptBadgeLabel, itemDisplayName, eventSummaryText);
return;
}
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray;
var hintBg = TryFindResource("HintBackground") as Brush ?? BrushFromHex("#F8FAFC");

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -47,9 +47,9 @@ public partial class ChatWindow
return preset.Description.Trim();
if (string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase))
return "선택 작업 유형에 맞춰 문서·데이터·파일 작업 흐름으로 이어집니다.";
return "선택 작업 유형에 맞춰 문서, 데이터, 파일 작업 흐름으로 이어집니다.";
return "선택 대화 주제에 맞춰 응답 방향과 초안 흐름을 정리합니다.";
return "선택 대화 주제에 맞춰 응답 방향과 초안 흐름을 정리합니다.";
}
private void UpdateFolderBar()
@@ -74,7 +74,7 @@ public partial class ChatWindow
}
else
{
FolderPathLabel.Text = "폴더를 선택하세요";
FolderPathLabel.Text = "폴더를 선택하세요.";
FolderPathLabel.ToolTip = null;
}
@@ -129,7 +129,7 @@ public partial class ChatWindow
memory.Load(workFolder);
var docs = memory.InstructionDocuments;
var learned = memory.All.Count;
var includePolicy = _settings.Settings.Llm.AllowExternalMemoryIncludes ? "외부 include 허용" : "외부 include 차단";
var includePolicy = _settings.Settings.Llm.AllowExternalMemoryIncludes ? "?몃? include ?덉슜" : "?몃? include 李⑤떒";
var auditEnabled = _settings.Settings.Llm.EnableAuditLog;
var recentIncludeEntries = AuditLogService.LoadRecent("MemoryInclude", maxCount: 5, daysBack: 3);
@@ -143,7 +143,7 @@ public partial class ChatWindow
var panel = new StackPanel { Margin = new Thickness(2) };
panel.Children.Add(new TextBlock
{
Text = "메모리 상태",
Text = "硫붾え由??곹깭",
FontSize = 13,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
@@ -151,7 +151,7 @@ public partial class ChatWindow
});
panel.Children.Add(new TextBlock
{
Text = $"계층형 규칙 {docs.Count}개 · 학습 메모리 {learned}개 · {includePolicy}",
Text = $"怨꾩링??洹쒖튃 {docs.Count}媛?쨌 ?숈뒿 硫붾え由?{learned}媛?쨌 {includePolicy}",
FontSize = 11.5,
Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap,
@@ -163,7 +163,7 @@ public partial class ChatWindow
panel.Children.Add(CreateSurfacePopupSeparator());
panel.Children.Add(new TextBlock
{
Text = "적용 중 규칙",
Text = "?곸슜 以?洹쒖튃",
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
@@ -177,7 +177,7 @@ public partial class ChatWindow
{
panel.Children.Add(new TextBlock
{
Text = $"{docs.Count - 6}개 규칙",
Text = $"??{docs.Count - 6}媛?洹쒖튃",
FontSize = 11,
Foreground = secondaryText,
Margin = new Thickness(8, 2, 8, 4),
@@ -188,7 +188,7 @@ public partial class ChatWindow
panel.Children.Add(CreateSurfacePopupSeparator());
panel.Children.Add(new TextBlock
{
Text = "최근 include 감사",
Text = "理쒓렐 include 媛먯궗",
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
@@ -199,7 +199,7 @@ public partial class ChatWindow
{
panel.Children.Add(new TextBlock
{
Text = "감사 로그가 꺼져 있어 include 이력은 기록되지 않습니다.",
Text = "媛먯궗 濡쒓렇媛€ 爰쇱졇 ?덉뼱 include ?대젰?€ 湲곕줉?섏? ?딆뒿?덈떎.",
FontSize = 11,
Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap,
@@ -210,7 +210,7 @@ public partial class ChatWindow
{
panel.Children.Add(new TextBlock
{
Text = "최근 3일간 include 감사 기록이 없습니다.",
Text = "理쒓렐 3?쇨컙 include 媛먯궗 湲곕줉???놁뒿?덈떎.",
FontSize = 11,
Foreground = secondaryText,
Margin = new Thickness(8, 0, 8, 6),
@@ -245,7 +245,7 @@ public partial class ChatWindow
return path;
var directory = Path.GetDirectoryName(path);
return string.IsNullOrWhiteSpace(directory) ? fileName : $"{fileName} · {directory}";
return string.IsNullOrWhiteSpace(directory) ? fileName : $"{fileName} {directory}";
}
catch
{
@@ -266,14 +266,14 @@ public partial class ChatWindow
var stack = new StackPanel { Margin = new Thickness(8, 2, 8, 4) };
stack.Children.Add(new TextBlock
{
Text = $"[{doc.Label}] 우선순위 {doc.Priority}",
Text = $"[{doc.Label}] ?곗꽑?쒖쐞 {doc.Priority}",
FontSize = 11.5,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
});
stack.Children.Add(new TextBlock
{
Text = meta.Count == 0 ? doc.Layer : $"{doc.Layer} · {string.Join(" · ", meta)}",
Text = meta.Count == 0 ? doc.Layer : $"{doc.Layer} {string.Join(" ", meta)}",
FontSize = 10.5,
Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap,
@@ -297,13 +297,13 @@ public partial class ChatWindow
private Border BuildMemoryPopupAuditRow(AuditEntry entry, Brush primaryText, Brush secondaryText, Brush okBrush, Brush warnBrush, Brush dangerBrush)
{
var statusBrush = entry.Success ? okBrush : dangerBrush;
var statusText = entry.Success ? "허용" : "차단";
var statusText = entry.Success ? "?덉슜" : "李⑤떒";
var resultBrush = entry.Success ? secondaryText : warnBrush;
var stack = new StackPanel { Margin = new Thickness(8, 2, 8, 4) };
stack.Children.Add(new TextBlock
{
Text = $"{statusText} · {entry.Timestamp:HH:mm:ss}",
Text = $"{statusText} {entry.Timestamp:HH:mm:ss}",
FontSize = 11.5,
FontWeight = FontWeights.SemiBold,
Foreground = statusBrush,
@@ -359,8 +359,9 @@ public partial class ChatWindow
!string.IsNullOrWhiteSpace(m.Content) &&
(string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase) ||
string.Equals(m.Role, "assistant", StringComparison.OrdinalIgnoreCase))) == true;
var hasVisibleExecution = conversation?.ExecutionEvents?.Count > 0;
if (string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase) || hasVisibleMessages || _isStreaming)
if (string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase) || hasVisibleMessages || hasVisibleExecution || _isStreaming)
{
SelectedPresetGuide.Visibility = Visibility.Collapsed;
SelectedPresetGuideTitle.Text = "";
@@ -394,3 +395,4 @@ public partial class ChatWindow
SelectedPresetGuide.Visibility = Visibility.Visible;
}
}

View File

@@ -117,6 +117,11 @@ public partial class ChatWindow
{
_transcriptElements.Clear();
_transcriptElementMap.Clear();
_lastGroupedProcessFeedKey = null;
_lastGroupedProcessFeedIndex = -1;
_processFeedAppendCount = 0;
_processFeedMergeCount = 0;
_transcriptRowKindCounts.Clear();
}
private void RemoveTranscriptElement(UIElement element)

View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Linq;
using System.Windows.Threading;
using AxCopilot.Models;
using AxCopilot.Services;
@@ -95,6 +96,11 @@ public partial class ChatWindow
renderedItems = renderPlan.NewKeys.Count,
hiddenCount = renderPlan.HiddenCount,
transcriptElements = GetTranscriptElementCount(),
processFeedAppends = _processFeedAppendCount,
processFeedMerges = _processFeedMergeCount,
rowKindCounts = _transcriptRowKindCounts.ToDictionary(
pair => pair.Key.ToString(),
pair => pair.Value),
});
}