에이전트 진행 표시 구조를 claude-code식 row 기반으로 재정리
Some checks failed
Release Gate / gate (push) Has been cancelled
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:
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user