에이전트 진행 표시 구조를 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

@@ -2,6 +2,28 @@ using System;
namespace AxCopilot.Services.Agent;
internal enum TranscriptRowKind
{
AssistantText,
Thinking,
ToolActivity,
Permission,
ToolResult,
CompactBoundary,
Waiting,
PlanApproval,
Status,
}
internal sealed record AgentTranscriptRowPresentation(
TranscriptRowKind Kind,
string BadgeLabel,
string Title,
string Description,
string GroupKey,
bool CanGroup,
bool Emphasize);
internal static class AgentTranscriptDisplayCatalog
{
public static string GetDisplayName(string? rawName, bool slashPrefix = false)
@@ -15,20 +37,27 @@ internal static class AgentTranscriptDisplayCatalog
{
"file_read" => "파일 읽기",
"file_write" => "파일 쓰기",
"file_edit" => "파일 편집",
"file_watch" => "파일 변경 감시",
"file_edit" => "파일 수정",
"file_watch" => "파일 감시",
"file_info" => "파일 정보",
"file_manage" => "파일 관리",
"glob" => "파일 찾기",
"grep" => "내용 검색",
"folder_map" => "폴더 구조",
"multi_read" => "다중 파일 읽기",
"document_reader" => "문서 읽기",
"document_planner" => "문서 계획",
"document_assembler" => "문서 조합",
"document_read" or "document_reader" => "문서 읽기",
"document_plan" or "document_planner" => "문서 계획",
"document_assemble" or "document_assembler" => "문서 조합",
"document_review" => "문서 검토",
"format_convert" => "형식 변환",
"template_render" => "템플릿 렌더",
"html_create" => "HTML 생성",
"docx_create" => "Word 생성",
"markdown_create" or "md_create" => "Markdown 생성",
"excel_create" or "xlsx_create" => "Excel 생성",
"csv_create" => "CSV 생성",
"pptx_create" => "PowerPoint 생성",
"build_run" => "빌드/실행",
"test_loop" => "테스트 루프",
@@ -42,11 +71,11 @@ internal static class AgentTranscriptDisplayCatalog
"powershell" => "PowerShell",
"web_fetch" => "웹 요청",
"http" => "HTTP 요청",
"user_ask" => "의견 요청",
"user_ask" => "질문 요청",
"suggest_actions" => "다음 작업 제안",
"task_create" => "작업 생성",
"task_update" => "작업 업데이트",
"task_update" => "작업 갱신",
"task_list" => "작업 목록",
"task_get" => "작업 조회",
"task_stop" => "작업 중지",
@@ -104,7 +133,7 @@ internal static class AgentTranscriptDisplayCatalog
AgentEventType.ToolResult => evt.Success ? $"{displayName} 실행 완료" : $"{displayName} 실행 실패",
AgentEventType.SkillCall => $"{displayName} 실행",
AgentEventType.PermissionRequest => $"{displayName} 실행 전에 권한 확인이 필요합니다.",
AgentEventType.PermissionGranted => $"{displayName} 실행 권한이 승인되었습니다.",
AgentEventType.PermissionGranted => $"{displayName} 실행 권한을 확인했습니다.",
AgentEventType.PermissionDenied => $"{displayName} 실행 권한이 거부되었습니다.",
AgentEventType.Complete => "에이전트 작업이 완료되었습니다.",
AgentEventType.Error => "에이전트 실행 중 오류가 발생했습니다.",
@@ -112,6 +141,257 @@ internal static class AgentTranscriptDisplayCatalog
};
}
public static AgentTranscriptRowPresentation ResolveRowPresentation(AgentEvent evt, string itemDisplayName, string transcriptBadgeLabel)
{
var summary = (evt.Summary ?? string.Empty).Trim();
var toolName = (evt.ToolName ?? string.Empty).Trim().ToLowerInvariant();
var resultPresentation = evt.Type == AgentEventType.ToolResult
? ToolResultPresentationCatalog.Resolve(evt, transcriptBadgeLabel)
: null;
var permissionPresentation = evt.Type switch
{
AgentEventType.PermissionRequest => PermissionRequestPresentationCatalog.Resolve(evt.ToolName, pending: true),
AgentEventType.PermissionGranted => PermissionRequestPresentationCatalog.Resolve(evt.ToolName, pending: false),
_ => null
};
if (string.Equals(toolName, "agent_wait", StringComparison.OrdinalIgnoreCase))
{
return new AgentTranscriptRowPresentation(
TranscriptRowKind.Waiting,
"대기",
"처리 중...",
string.IsNullOrWhiteSpace(summary) ? "작업을 계속 진행하기 위한 응답을 기다리는 중입니다." : summary,
"waiting:agent",
true,
true);
}
if (string.Equals(toolName, "context_compaction", StringComparison.OrdinalIgnoreCase))
{
return new AgentTranscriptRowPresentation(
TranscriptRowKind.CompactBoundary,
"압축",
"컨텍스트 압축 중...",
string.IsNullOrWhiteSpace(summary) ? "긴 대화를 계속 진행하기 위해 컨텍스트를 정리하고 있습니다." : summary,
"compact:context",
true,
true);
}
if (evt.Type == AgentEventType.Planning)
{
var title = evt.Steps is { Count: > 0 }
? $"계획 {evt.Steps.Count}단계 정리"
: "작업 계획 정리 중";
return new AgentTranscriptRowPresentation(
TranscriptRowKind.Thinking,
"계획",
title,
string.IsNullOrWhiteSpace(summary) ? "실행 순서와 필요한 도구를 정리하고 있습니다." : summary,
"planning",
true,
false);
}
if (evt.Type == AgentEventType.Decision)
{
return new AgentTranscriptRowPresentation(
TranscriptRowKind.PlanApproval,
"확인",
"계획 확인 필요",
string.IsNullOrWhiteSpace(summary) ? "사용자 확인이 필요한 계획 단계입니다." : summary,
"plan:approval",
false,
true);
}
if (evt.Type is AgentEventType.StepStart or AgentEventType.StepDone)
{
var title = evt.StepTotal > 0
? evt.Type == AgentEventType.StepStart
? $"{evt.StepCurrent}/{evt.StepTotal} 단계 진행"
: $"{evt.StepCurrent}/{evt.StepTotal} 단계 완료"
: evt.Type == AgentEventType.StepStart ? "단계 진행" : "단계 완료";
return new AgentTranscriptRowPresentation(
TranscriptRowKind.ToolActivity,
"단계",
title,
string.IsNullOrWhiteSpace(summary) ? title : summary,
$"step:{evt.StepCurrent}:{evt.StepTotal}:{evt.Type}",
false,
false);
}
if (evt.Type == AgentEventType.Thinking)
{
var title = ResolveThinkingTitle(summary, toolName);
return new AgentTranscriptRowPresentation(
TranscriptRowKind.Thinking,
"생각",
title,
string.IsNullOrWhiteSpace(summary) ? title : summary,
$"thinking:{ResolveActivityGroup(toolName, summary)}",
true,
false);
}
if (evt.Type == AgentEventType.ToolCall || evt.Type == AgentEventType.SkillCall)
{
var group = ResolveActivityGroup(toolName, summary);
var title = BuildActivityTitle(toolName, itemDisplayName, summary);
var badge = evt.Type == AgentEventType.SkillCall ? "스킬" : "도구";
return new AgentTranscriptRowPresentation(
TranscriptRowKind.ToolActivity,
badge,
title,
BuildActivityDescription(group, summary),
$"activity:{group}",
true,
false);
}
if (permissionPresentation != null)
{
return new AgentTranscriptRowPresentation(
TranscriptRowKind.Permission,
"권한",
permissionPresentation.Label,
permissionPresentation.Description,
$"permission:{permissionPresentation.Kind}",
false,
true);
}
if (resultPresentation != null)
{
return new AgentTranscriptRowPresentation(
TranscriptRowKind.ToolResult,
"결과",
resultPresentation.Label,
resultPresentation.Description,
$"result:{resultPresentation.Kind}:{resultPresentation.StatusKind}",
false,
resultPresentation.NeedsAttention);
}
if (evt.Type == AgentEventType.Error)
{
return new AgentTranscriptRowPresentation(
TranscriptRowKind.Status,
"오류",
"실행 중 오류 발생",
string.IsNullOrWhiteSpace(summary) ? "에이전트 실행 중 오류가 발생했습니다." : summary,
"status:error",
false,
true);
}
if (evt.Type == AgentEventType.Complete)
{
return new AgentTranscriptRowPresentation(
TranscriptRowKind.Status,
"완료",
"작업 완료",
string.IsNullOrWhiteSpace(summary) ? "에이전트 작업이 완료되었습니다." : summary,
"status:complete",
false,
false);
}
return new AgentTranscriptRowPresentation(
TranscriptRowKind.Status,
transcriptBadgeLabel,
string.IsNullOrWhiteSpace(summary) ? transcriptBadgeLabel : summary,
string.IsNullOrWhiteSpace(summary) ? transcriptBadgeLabel : summary,
$"status:{evt.Type}",
false,
false);
}
private static string ResolveThinkingTitle(string summary, string toolName)
{
if (summary.Contains("검증", StringComparison.OrdinalIgnoreCase))
return "결과 검증 중...";
if (summary.Contains("diff", StringComparison.OrdinalIgnoreCase))
return "변경 내용 확인 중...";
if (summary.Contains("retry", StringComparison.OrdinalIgnoreCase) || summary.Contains("재시도", StringComparison.OrdinalIgnoreCase))
return "재시도 준비 중...";
if (summary.Contains("fallback", StringComparison.OrdinalIgnoreCase) || summary.Contains("자동 생성", StringComparison.OrdinalIgnoreCase))
return "대체 경로 준비 중...";
if (toolName.Contains("document"))
return "문서 흐름 정리 중...";
return string.IsNullOrWhiteSpace(summary) ? "분석 중..." : summary;
}
private static string ResolveActivityGroup(string toolName, string summary)
{
if (toolName is "file_read" or "document_read" or "glob" or "grep" or "folder_map" or "multi_read")
return "read";
if (toolName is "file_edit" or "file_write")
return "edit";
if (toolName.Contains("build") || toolName.Contains("test") || toolName == "process")
return "execute";
if (toolName.Contains("git") || toolName.Contains("diff"))
return "git";
if (toolName.Contains("document") || toolName.Contains("html_create") || toolName.Contains("docx_create") || toolName.Contains("markdown_create"))
return "document";
if (toolName.Contains("web") || toolName.Contains("http"))
return "web";
if (summary.Contains("권한", StringComparison.OrdinalIgnoreCase))
return "permission";
return "general";
}
private static string BuildActivityTitle(string toolName, string itemDisplayName, string summary)
{
if (toolName is "multi_read")
return "여러 파일 읽는 중";
if (toolName is "file_read" or "document_read")
return "파일 읽는 중";
if (toolName is "glob")
return "관련 파일 찾는 중";
if (toolName is "grep")
return "코드 검색 중";
if (toolName is "folder_map")
return "구조 확인 중";
if (toolName is "file_edit")
return "파일 수정 중";
if (toolName is "file_write")
return "파일 작성 중";
if (toolName.Contains("build") || toolName.Contains("test"))
return "빌드/테스트 실행 중";
if (toolName == "process" || toolName == "bash" || toolName == "powershell")
return "명령 실행 중";
if (toolName.Contains("git") || toolName.Contains("diff"))
return "Git 작업 중";
if (toolName.Contains("document") || toolName.Contains("html_create") || toolName.Contains("docx_create"))
return "문서 결과 만드는 중";
if (toolName.Contains("web") || toolName.Contains("http"))
return "웹 정보 확인 중";
if (!string.IsNullOrWhiteSpace(itemDisplayName))
return $"{itemDisplayName} 실행 중";
return string.IsNullOrWhiteSpace(summary) ? "도구 실행 중" : summary;
}
private static string BuildActivityDescription(string group, string summary)
{
if (!string.IsNullOrWhiteSpace(summary))
return summary;
return group switch
{
"read" => "질문과 관련된 파일과 내용만 추려서 확인하고 있습니다.",
"edit" => "필요한 변경만 적용하고 결과를 다시 확인하고 있습니다.",
"execute" => "실행 결과와 로그를 확인해 다음 단계를 판단하고 있습니다.",
"git" => "변경 범위와 저장소 상태를 확인하고 있습니다.",
"document" => "문서 산출물을 준비하고 있습니다.",
"web" => "필요한 외부 정보를 확인하고 있습니다.",
_ => "다음 단계를 진행하기 위한 작업을 실행하고 있습니다."
};
}
private static string GetToolCategoryLabel(string? rawName)
{
if (string.IsNullOrWhiteSpace(rawName))
@@ -119,13 +399,13 @@ internal static class AgentTranscriptDisplayCatalog
return rawName.Trim().ToLowerInvariant() switch
{
"file_read" or "file_write" or "file_edit" or "glob" or "grep" or "folder_map" or "file_watch" or "file_info" or "file_manage"
"file_read" or "file_write" or "file_edit" or "glob" or "grep" or "folder_map" or "file_watch" or "file_info" or "file_manage" or "multi_read"
=> "파일",
"build_run" or "test_loop" or "dev_env_detect"
=> "빌드",
"git_tool" or "diff_tool" or "diff_preview"
=> "Git",
"document_reader" or "document_planner" or "document_assembler" or "document_review" or "format_convert" or "template_render"
"document_read" or "document_reader" or "document_plan" or "document_planner" or "document_assemble" or "document_assembler" or "document_review" or "format_convert" or "template_render" or "html_create" or "docx_create"
=> "문서",
"user_ask"
=> "질문",

View File

@@ -1,3 +1,5 @@
using System;
namespace AxCopilot.Services.Agent;
internal sealed record PermissionRequestPresentation(
@@ -20,104 +22,104 @@ internal static class PermissionRequestPresentationCatalog
if (tool.Contains("bash"))
return Build("bash", pending, "\uE756",
"Bash 실행 권한 요청", "Bash 실행 확인",
" 명령을 실행하기 전에 확인이 필요합니다.",
"Bash 실행이 승인되어 계속 진행합니다.",
"명령과 작업 위치를 확인하세요.",
"Bash 명령을 실행하기 전에 사용자 확인이 필요합니다.",
"Bash 실행을 확인해 주시면 같은 흐름으로 이어집니다.",
"명령과 작업 위치를 먼저 확인해 주세요.",
"#FEF2F2", "#DC2626", "high", false);
if (tool.Contains("powershell"))
return Build("powershell", pending, "\uE756",
"PowerShell 권한 요청", "PowerShell 실행 확인",
"PowerShell 명령을 실행하기 전에 확인이 필요합니다.",
"PowerShell 실행 승인되어 계속 진행합니다.",
"스크립트와 실행 범위를 확인세요.",
"PowerShell 명령을 실행하기 전에 사용자 확인이 필요합니다.",
"PowerShell 실행 승인하면 같은 작업을 이어서 진행합니다.",
"스크립트와 실행 범위를 확인해 주세요.",
"#FEF2F2", "#DC2626", "high", false);
if (tool.Contains("process") || tool.Contains("build") || tool.Contains("test"))
return Build("command", pending, "\uE756",
"명령 실행 권한 요청", "명령 실행 확인",
"명령 또는 빌드 작업 실행 전에 확인이 필요합니다.",
"명령 실행 승인되어 계속 진행합니다.",
"실행 명령과 영향 범위를 확인하세요.",
"명령이나 빌드 작업 실행하기 전에 확인이 필요합니다.",
"명령 실행 승인하면 현재 흐름을 이어서 진행합니다.",
"실행 명령과 영향 범위를 먼저 확인해 주세요.",
"#FEF2F2", "#DC2626", "high", false);
if (tool.Contains("web") || tool.Contains("fetch") || tool.Contains("http"))
return Build("web_fetch", pending, "\uE774",
"웹 요청 권한 요청", "웹 요청 확인",
"외부 요청을 보내기 전에 확인이 필요합니다.",
"웹 요청이 승인되어 계속 진행합니다.",
"조회 URL과 전송 범위를 확인세요.",
"외부 요청을 보내기 전에 확인이 필요합니다.",
"요청 대상을 확인해 주시면 이어서 처리합니다.",
"조회 URL과 전송 범위를 확인해 주세요.",
"#FFF7ED", "#C2410C", "medium", false);
if (tool.Contains("mcp"))
return Build("mcp", pending, "\uE943",
"MCP 도구 권한 요청", "MCP 도구 확인",
"연결된 MCP 도구 사용 전에 확인이 필요합니다.",
"MCP 도구 사용 승인되어 계속 진행합니다.",
"서버와 호출 도를 확인세요.",
"연결된 MCP 도구 사용하기 전에 확인이 필요합니다.",
"MCP 도구 사용 승인하면 흐름을 이어서 진행합니다.",
"서버와 호출 도를 확인해 주세요.",
"#F5F3FF", "#7C3AED", "medium", false);
if (tool.Contains("skill"))
return Build("skill", pending, "\uE8A5",
"스킬 실행 권한 요청", "스킬 실행 확인",
"연결된 스킬을 실행하기 전에 확인이 필요합니다.",
"스킬 실행 승인되어 계속 진행합니다.",
"용 도구와 실행 컨텍스트를 확인세요.",
"스킬 실행 승인하면 같은 작업 흐름을 이어갑니다.",
"용 도구와 실행 컨텍스트를 확인해 주세요.",
"#F5F3FF", "#7C3AED", "medium", false);
if (tool.Contains("ask"))
return Build("question", pending, "\uE897",
"의견 요청 확인", "의견 요청 완료",
"사용자에게 선택이나 답변을 요청합니다.",
"사용자 답변을 받 다음 단계로 진행합니다.",
"질문 의도와 선택지를 확인세요.",
"질문 요청 확인", "질문 요청 완료",
"사용자에게 추가 질문이나 선택을 요청합니다.",
"응답을 받으면 다음 단계로 자동으로 이어집니다.",
"질문 의도와 선택지를 확인해 주세요.",
"#EFF6FF", "#2563EB", "low", false);
if (tool.Contains("file_edit") || tool.Contains("edit"))
return Build("file_edit", pending, "\uE70F",
"파일 수정 권한 요청", "파일 수정 확인",
"파일을 변경하기 전에 확인이 필요합니다.",
"파일 수정이 승인되어 계속 진행합니다.",
"변경 diff와 대상 파일을 확인하세요.",
"파일을 변경하기 전에 사용자 확인이 필요합니다.",
"변경 내용을 확인하면 같은 흐름으로 이어집니다.",
"변경 diff와 대상 파일을 먼저 확인해 주세요.",
"#FFF7ED", "#C2410C", "high", true);
if (tool.Contains("file_write") || tool.Contains("write"))
return Build("file_write", pending, "\uE70F",
"파일 쓰기 권한 요청", "파일 쓰기 확인",
"새 파일 작성 또는 덮어쓰기 전에 확인이 필요합니다.",
"파일 쓰기 승인되어 계속 진행합니다.",
"성 위치와 내용 미리보기를 확인세요.",
"새 파일 생성이나 덮어쓰기 전에 사용자 확인이 필요합니다.",
"파일 쓰기 승인하면 같은 흐름을 이어갑니다.",
"성 위치와 내용 미리보기를 확인해 주세요.",
"#FFF7ED", "#C2410C", "high", true);
if (tool.Contains("git"))
return Build("git", pending, "\uE8A7",
"Git 작업 권한 요청", "Git 작업 확인",
"브랜치나 커밋 상태를 바꾸기 전에 확인이 필요합니다.",
"Git 작업 승인되어 계속 진행합니다.",
"브랜치와 변경 범위를 확인세요.",
"브랜치나 작업트리 상태를 바꾸기 전에 확인이 필요합니다.",
"Git 작업 승인하면 같은 흐름을 이어갑니다.",
"브랜치와 변경 범위를 확인해 주세요.",
"#EFF6FF", "#2563EB", "medium", false);
if (tool.Contains("document") || tool.Contains("template") || tool.Contains("format"))
return Build("document", pending, "\uE8A5",
"문서 작업 권한 요청", "문서 작업 확인",
"문서 생성 또는 변환 작업 전에 확인이 필요합니다.",
"문서 작업 승인되어 계속 진행합니다.",
"출력 형식과 저장 위치를 확인세요.",
"문서 생성이나 변환 작업 전에 확인이 필요합니다.",
"문서 작업 승인하면 같은 흐름을 이어갑니다.",
"출력 형식과 대상 위치를 확인해 주세요.",
"#FFF7ED", "#C2410C", "medium", false);
if (tool.Contains("file") || tool.Contains("glob") || tool.Contains("grep") || tool.Contains("folder"))
return Build("filesystem", pending, "\uE8A5",
"파일 접근 권한 요청", "파일 접근 확인",
"폴더나 파일 내용을 읽기 전에 확인이 필요합니다.",
"파일 접근 승인되어 계속 진행합니다.",
"읽기 범위와 접근 경로를 확인세요.",
"파일이나 폴더를 읽기 전에 확인이 필요합니다.",
"파일 접근 승인하면 같은 흐름으로 이어집니다.",
"읽기 범위와 경로를 확인해 주세요.",
"#FFF7ED", "#C2410C", "medium", false);
return Build("generic", pending, "\uE897",
"권한 요청", "권한 확인",
"계속 진행하기 전에 사용자 확인이 필요합니다.",
"요청 승인되어 계속 진행합니다.",
"실행 의도와 대상 범위를 확인세요.",
"계속 진행하기 전에 사용자 확인이 필요합니다.",
"요청 승인하면 같은 흐름으로 이어집니다.",
"실행 의도와 대상 범위를 확인해 주세요.",
"#FFF7ED", "#C2410C", "medium", false);
}

View File

@@ -1,3 +1,5 @@
using System;
namespace AxCopilot.Services.Agent;
internal sealed record ToolResultPresentation(
@@ -29,7 +31,7 @@ internal static class ToolResultPresentationCatalog
"\uE711",
$"{baseLabel} 취소",
"요청이 중단되어 결과가 취소되었습니다.",
"필요하면 같은 요청을 다시 실행하세요.",
"필요하면 같은 요청을 다시 실행할 수 있습니다.",
"#F8FAFC",
"#475569",
"cancel",
@@ -45,7 +47,7 @@ internal static class ToolResultPresentationCatalog
"\uE783",
$"{baseLabel} 거부",
"권한이 거부되어 작업이 중단되었습니다.",
"권한 모드를 바꾸거나 다시 인하면 이어서 진행할 수 있습니다.",
"권한 모드를 바꾸거나 다시 인하면 이어서 진행할 수 있습니다.",
"#FEF2F2",
"#DC2626",
"reject",
@@ -58,8 +60,8 @@ internal static class ToolResultPresentationCatalog
return new ToolResultPresentation(
kind,
"\uE8D7",
$"{baseLabel} 승인 대기",
"다음 단계로 진행하려면 사용자 승인이 필요합니다.",
$"{baseLabel} 승인 필요",
"다음 단계로 진행하려면 사용자 승인이나 확인이 필요합니다.",
"승인 후 같은 작업 흐름이 이어집니다.",
"#FFF7ED",
"#C2410C",
@@ -74,8 +76,8 @@ internal static class ToolResultPresentationCatalog
kind,
"\uE7BA",
$"{baseLabel} 부분 완료",
"일부 단계만 완료되어 후속 확인이나 재실행이 필요할 수 있습니다.",
"남은 단계나 누락된 결과를 확인세요.",
"일부 단계만 완료되어 후속 확인이나 재시도가 필요할 수 있습니다.",
"후속 단계와 파일 결과를 확인해 주세요.",
"#FFFBEA",
"#A16207",
"partial",
@@ -114,7 +116,7 @@ internal static class ToolResultPresentationCatalog
return "file_edit";
if (tool.Contains("file_write"))
return "file_write";
if (tool.Contains("file_read") || tool.Contains("glob") || tool.Contains("grep"))
if (tool.Contains("file_read") || tool.Contains("glob") || tool.Contains("grep") || tool.Contains("folder_map") || tool.Contains("multi_read"))
return "filesystem";
if (tool.Contains("file"))
return "file";
@@ -141,111 +143,93 @@ internal static class ToolResultPresentationCatalog
return "generic";
}
private static string BuildSuccessLabel(string kind, string baseLabel)
private static string BuildSuccessLabel(string kind, string baseLabel) => kind switch
{
return kind switch
{
"file_edit" => "파일 수정 완료",
"file_write" => "파일 쓰기 완료",
"filesystem" => "파일 탐색 완료",
"file" => "파일 작업 완료",
"build_test" => "빌드/테스트 완료",
"git" => "Git 작업 완료",
"document" => "문서 작업 완료",
"skill" => "스킬 실행 완료",
"mcp" => "MCP 도구 완료",
"question" => "의견 요청 완료",
"web" => "웹 요청 완료",
"command" => "명령 실행 완료",
_ => baseLabel,
};
}
"file_edit" => "파일 수정 완료",
"file_write" => "파일 쓰기 완료",
"filesystem" => "파일 탐색 완료",
"file" => "파일 작업 완료",
"build_test" => "빌드/테스트 완료",
"git" => "Git 작업 완료",
"document" => "문서 작업 완료",
"skill" => "스킬 실행 완료",
"mcp" => "MCP 도구 완료",
"question" => "질문 요청 완료",
"web" => "웹 요청 완료",
"command" => "명령 실행 완료",
_ => baseLabel,
};
private static string BuildFailureLabel(string kind, string baseLabel)
private static string BuildFailureLabel(string kind, string baseLabel) => kind switch
{
return kind switch
{
"file_edit" => "파일 수정 실패",
"file_write" => "파일 쓰기 실패",
"filesystem" => "파일 탐색 실패",
"file" => "파일 작업 실패",
"build_test" => "빌드/테스트 실패",
"git" => "Git 작업 실패",
"document" => "문서 작업 실패",
"skill" => "스킬 실행 실패",
"mcp" => "MCP 도구 실패",
"question" => "의견 요청 실패",
"web" => "웹 요청 실패",
"command" => "명령 실행 실패",
_ => $"{baseLabel} 실패",
};
}
"file_edit" => "파일 수정 실패",
"file_write" => "파일 쓰기 실패",
"filesystem" => "파일 탐색 실패",
"file" => "파일 작업 실패",
"build_test" => "빌드/테스트 실패",
"git" => "Git 작업 실패",
"document" => "문서 작업 실패",
"skill" => "스킬 실행 실패",
"mcp" => "MCP 도구 실패",
"question" => "질문 요청 실패",
"web" => "웹 요청 실패",
"command" => "명령 실행 실패",
_ => $"{baseLabel} 실패",
};
private static string BuildSuccessDescription(string kind)
private static string BuildSuccessDescription(string kind) => kind switch
{
return kind switch
{
"file_edit" => "파일 수정 결과가 저장되었습니다.",
"file_write" => "파일 작성 결과가 저장되었습니다.",
"filesystem" => "파일과 폴더 정보를 성공적으로 읽었습니다.",
"file" => "파일 관련 작업이 정상적으로 끝났습니다.",
"build_test" => "빌드 또는 테스트 단계가 성공적으로 끝났습니다.",
"git" => "Git 관련 작업이 정상적으로 끝났습니다.",
"document" => "문서 생성 또는 변환 작업이 완료되었습니다.",
"skill" => "선택한 스킬이 정상적으로 실행되었습니다.",
"mcp" => "등록된 MCP 도구 호출이 성공적으로 끝났습니다.",
"question" => "사용자 응답을 받아 다음 단계로 넘어갈 수 있습니다.",
"web" => "요청이 정상적으로 끝났습니다.",
"command" => "명령 실행이 정상적으로 끝났습니다.",
_ => "요청한 작업이 정상적으로 완료되었습니다.",
};
}
"file_edit" => "파일 수정 결과가 정상적으로 반영되었습니다.",
"file_write" => "새 파일 생성이나 쓰기 작업이 완료되었습니다.",
"filesystem" => "질문과 관련된 파일과 구조 정보를 찾았습니다.",
"file" => "파일 관련 작업이 정상적으로 끝났습니다.",
"build_test" => "빌드나 테스트 단계가 정상적으로 끝났습니다.",
"git" => "저장소 상태 확인 또는 변경 작업이 완료되었습니다.",
"document" => "문서 산출물 생성 또는 조립이 완료되었습니다.",
"skill" => "선택한 스킬이 정상적으로 실행되었습니다.",
"mcp" => "등록된 MCP 도구 호출이 완료되었습니다.",
"question" => "사용자 응답을 받아 다음 단계로 이어갈 수 있습니다.",
"web" => "필요한 웹 정보 조회가 완료되었습니다.",
"command" => "명령 실행이 완료되어 결과를 확인할 수 있습니다.",
_ => "요청한 작업이 정상적으로 완료되었습니다.",
};
private static string BuildFailureDescription(string kind)
private static string BuildFailureDescription(string kind) => kind switch
{
return kind switch
{
"file_edit" => "파일 변경 과정에서 문제가 발생했습니다.",
"file_write" => "파일 작성 또는 저장 과정에서 문제가 발생했습니다.",
"filesystem" => "파일/폴더 접근 중 문제가 발생했습니다.",
"file" => "파일 처리문제가 발생했습니다.",
"build_test" => "빌드 또는 테스트 단계에서 실패가 발생했습니다.",
"git" => "Git 관련 작업이 실패했습니다.",
"document" => "문서 생성 또는 변환 작업이 실패했습니다.",
"skill" => "스킬 실행 중 문제가 발생했습니다.",
"mcp" => "MCP 도구 호출 중 문제가 발생했습니다.",
"question" => "사용자 의견 요청 과정에서 문제가 발생했습니다.",
"web" => "웹 요청 처리에 실패했습니다.",
"command" => "명령 실행 중 오류가 발생했습니다.",
_ => "작업 처리 중 오류가 발생했습니다.",
};
}
"file_edit" => "파일 변경 과정에서 문제가 발생했습니다.",
"file_write" => "파일 생성 또는 쓰기 과정에서 문제가 발생했습니다.",
"filesystem" => "파일이나 폴더를 확인하는 과정에서 문제가 발생했습니다.",
"file" => "파일 처리 중 문제가 발생했습니다.",
"build_test" => "빌드 또는 테스트 단계에서 실패가 발생했습니다.",
"git" => "Git 작업오류가 발생했습니다.",
"document" => "문서 생성 또는 조합 과정이 실패했습니다.",
"skill" => "스킬 실행 중 문제가 발생했습니다.",
"mcp" => "MCP 도구 호출 중 문제가 발생했습니다.",
"question" => "사용자 질문/응답 처리 단계에서 문제가 발생했습니다.",
"web" => "웹 요청 처리에 실패했습니다.",
"command" => "명령 실행 중 오류가 발생했습니다.",
_ => "작업 처리 중 오류가 발생했습니다.",
};
private static string BuildSuccessFollowUp(string kind)
private static string BuildSuccessFollowUp(string kind) => kind switch
{
return kind switch
{
"file_edit" or "file_write" => "변경 내용을 preview나 diff에서 다시 확인할 수 있습니다.",
"build_test" => "출력 로그와 후속 수정 필요 여부를 확인세요.",
"git" => "브랜치 상태나 변경 요약을 이어서 확인하세요.",
"document" => "생성된 산출물 경로를 열어 결과를 확인하세요.",
"skill" => "같은 스킬을 다른 입력으로 이어서 실행할 수 있습니다.",
_ => "필요하면 후속 요청을 이어서 실행할 수 있습니다.",
};
}
"file_edit" or "file_write" => "변경 내용은 preview나 diff에서 다시 확인할 수 있습니다.",
"build_test" => "출력 로그와 후속 수정 필요 여부를 확인해 주세요.",
"git" => "브랜치와 변경 수치를 이어서 확인해 주세요.",
"document" => "생성된 산출물 경로를 열어 결과를 확인해 주세요.",
"skill" => "같은 스킬을 다른 입력으로 이어서 실행할 수 있습니다.",
_ => "필요하면 다음 요청으로 이어서 작업할 수 있습니다.",
};
private static string BuildFailureFollowUp(string kind)
private static string BuildFailureFollowUp(string kind) => kind switch
{
return kind switch
{
"file_edit" or "file_write" => "대상 파일 경로와 권한, diff를 다시 확인세요.",
"build_test" => "실패 로그와 컴파일 오류 메시지를 먼저 확인하세요.",
"git" => "현재 브랜치, 잠금 상태, 충돌 여부를 확인하세요.",
"document" => "입력 데이터와 출력 형식, 저장 위치를 다시 확인세요.",
"skill" => "허용 도구와 런타임 요구사항을 다시 확인세요.",
"web" => "연결 상태와 요청 대상 URL을 다시 확인하세요.",
"mcp" => "MCP 서버 연결 상태와 도구 등록 상태를 다시 확인하세요.",
_ => "같은 요청을 재시도하기 전에 원인 메시지를 먼저 확인하세요.",
};
}
"file_edit" or "file_write" => "대상 파일 경로와 권한, diff 결과를 다시 확인해 주세요.",
"build_test" => "실패 로그와 컴파일/테스트 오류 메시지를 먼저 확인해 주세요.",
"git" => "브랜치 상태와 충돌 여부를 다시 확인해 주세요.",
"document" => "입력 데이터, 출력 형식, 대상 위치를 다시 확인해 주세요.",
"skill" => "사용 도구와 스킬 요구사항을 다시 확인해 주세요.",
"web" => "연결 상태와 요청 URL을 다시 확인해 주세요.",
"mcp" => "MCP 서버 연결 상태와 도구 등록 상태를 다시 확인해 주세요.",
_ => "같은 요청을 다시 시도하기 전에 원인 메시지를 먼저 확인해 주세요.",
};
}

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),
});
}