에이전트 진행 표시 구조를 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"
=> "질문",