AX Agent transcript 권한·도구 결과 표현 정교화 및 이벤트 렌더 분리
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
- claw-code 기준으로 transcript display catalog를 파일/문서/빌드/Git/웹/질문/에이전트 축으로 재정의 - 권한 요청 presentation catalog를 명령 실행, 웹 요청, 스킬 실행, 의견 요청, 파일 수정, 파일 접근 타입으로 세분화 - tool result catalog를 성공/실패/거부/취소와 도구 종류에 따라 더 읽기 쉬운 한국어 결과 라벨로 정리 - CreateCompactEventPill, AddAgentEventBanner, GetDecisionBadgeMeta를 ChatWindow.AgentEventRendering.cs로 분리해 메인 창 코드 비중 축소 - README와 DEVELOPMENT 문서에 변경 목적과 검증 결과 반영 - 검증: 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:
@@ -1120,3 +1120,7 @@ MIT License
|
||||
- 업데이트: 2026-04-06 01:12 (KST)
|
||||
- AX Agent 코워크/코드의 `폴더 내 문서 활용`을 사용자 옵션에서 제거했다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml), [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml), [AgentSettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/AgentSettingsWindow.xaml) 에서 하단 버튼, 내부 설정 행, 구형 설정창 항목을 걷어냈다.
|
||||
- 런타임은 옵션이 아닌 자동 정책으로 유지한다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서 채팅은 `none`, 코워크는 `passive`, 코드는 `active`를 자동 적용하고, 더 이상 오버레이 저장 시 `FolderDataUsage`를 사용자 선택값으로 저장하지 않는다.
|
||||
- 업데이트: 2026-04-06 01:24 (KST)
|
||||
- `claw-code` 기준 transcript 품질 향상을 위해 권한 요청/도구 결과/도구 이름 display catalog를 다시 정리했다. [AgentTranscriptDisplayCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs)는 파일/문서/빌드/Git/웹/스킬/질문 카테고리를 더 명확한 한국어 display name과 badge label로 분류하고, [PermissionRequestPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs)는 `명령 실행 / 웹 요청 / 스킬 실행 / 의견 요청 / 파일 수정 / 파일 접근` 권한 요청을 타입별 presentation으로 나누도록 보강했다.
|
||||
- [ToolResultPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs)는 `success / error / reject / cancel`을 도구 종류에 따라 `파일 작업 완료`, `빌드/테스트 실패`, `웹 요청 거부`처럼 더 읽기 쉬운 결과 라벨로 바꾸도록 확장했다.
|
||||
- transcript renderer 분리 2차로 [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs)를 추가해 `CreateCompactEventPill`, `AddAgentEventBanner`, `GetDecisionBadgeMeta`를 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 밖으로 옮겼다. 이제 메인 파일은 대화 흐름과 상태 처리에 더 집중하고, 이벤트 배너 렌더는 별도 partial에서 관리한다.
|
||||
|
||||
@@ -4882,3 +4882,7 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
|
||||
- 업데이트: 2026-04-06 01:12 (KST)
|
||||
- 코워크/코드의 `폴더 내 문서 활용`은 사용자 제어 옵션에서 제거하고 탭별 자동 정책으로 고정했다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 에서 하단 데이터 활용 버튼과 AX Agent 내부 설정의 관련 row를 제거했고, [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml), [AgentSettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/AgentSettingsWindow.xaml) 의 대응 UI도 정리했다.
|
||||
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 는 이제 `GetAutomaticFolderDataUsage()`만 사용해 채팅=`none`, 코워크=`passive`, 코드=`active`를 적용한다. 오버레이 저장에서도 `llm.FolderDataUsage`를 더 이상 사용자 입력으로 덮어쓰지 않으며, UI 클릭/선택 변경 핸들러는 자동 정책 유지용 no-op 수준으로 축소했다.
|
||||
- 업데이트: 2026-04-06 01:24 (KST)
|
||||
- transcript display catalog를 `claw-code` 기준으로 정교화했다. [AgentTranscriptDisplayCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs)는 도구/스킬 이름과 badge label을 `파일 / 문서 / 빌드 / Git / 웹 / 질문 / 제안 / 에이전트` 축으로 재정의했고, summary fallback 문구도 더 자연스러운 한국어로 정리했다.
|
||||
- [PermissionRequestPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs)는 `명령 실행 / 웹 요청 / 스킬 실행 / 의견 요청 / 파일 수정 / 파일 접근` 권한 요청을 타입별 색상/라벨로 분기하게 바꿨다. [ToolResultPresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs)는 도구 결과를 `파일 작업`, `빌드/테스트`, `Git`, `문서`, `스킬`, `웹 요청`, `명령 실행` 기준으로 성공/실패 라벨을 더 세밀하게 반환한다.
|
||||
- 이벤트 배너 renderer를 [ChatWindow.AgentEventRendering.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs) 로 분리했다. 기존 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 있던 `CreateCompactEventPill`, `AddAgentEventBanner`, `GetDecisionBadgeMeta`를 별도 partial로 옮겨, 이후 `permission/tool-result/plan` 타입별 renderer 확장을 더 쉽게 할 수 있는 구조를 마련했다.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
internal static class AgentTranscriptDisplayCatalog
|
||||
@@ -5,30 +7,44 @@ internal static class AgentTranscriptDisplayCatalog
|
||||
public static string GetDisplayName(string? rawName, bool slashPrefix = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawName))
|
||||
return slashPrefix ? "/스킬" : "도구";
|
||||
return slashPrefix ? "/skill" : "도구";
|
||||
|
||||
var normalized = rawName.Trim();
|
||||
var mapped = normalized.ToLowerInvariant() switch
|
||||
var lowered = normalized.ToLowerInvariant();
|
||||
var mapped = lowered switch
|
||||
{
|
||||
"file_read" => "파일 읽기",
|
||||
"file_write" => "파일 쓰기",
|
||||
"file_edit" => "파일 편집",
|
||||
"file_watch" => "파일 변경 감시",
|
||||
"file_info" => "파일 정보",
|
||||
"file_manage" => "파일 관리",
|
||||
"glob" => "파일 찾기",
|
||||
"grep" => "내용 검색",
|
||||
"folder_map" => "폴더 구조",
|
||||
|
||||
"document_reader" => "문서 읽기",
|
||||
"document_planner" => "문서 계획",
|
||||
"document_assembler" => "문서 조합",
|
||||
"document_review" => "문서 검토",
|
||||
"format_convert" => "형식 변환",
|
||||
"code_search" => "코드 검색",
|
||||
"code_review" => "코드 리뷰",
|
||||
"template_render" => "템플릿 렌더",
|
||||
|
||||
"build_run" => "빌드/실행",
|
||||
"test_loop" => "테스트 루프",
|
||||
"dev_env_detect" => "개발 환경 점검",
|
||||
"git_tool" => "Git",
|
||||
"process" => "프로세스",
|
||||
"glob" => "파일 찾기",
|
||||
"grep" => "내용 검색",
|
||||
"folder_map" => "폴더 맵",
|
||||
"memory" => "메모리",
|
||||
"diff_tool" => "Diff",
|
||||
"diff_preview" => "Diff 미리보기",
|
||||
|
||||
"process" => "명령 실행",
|
||||
"bash" => "Bash",
|
||||
"powershell" => "PowerShell",
|
||||
"web_fetch" => "웹 요청",
|
||||
"http" => "HTTP 요청",
|
||||
"user_ask" => "의견 요청",
|
||||
"suggest_actions" => "다음 작업 제안",
|
||||
|
||||
"task_create" => "작업 생성",
|
||||
"task_update" => "작업 업데이트",
|
||||
"task_list" => "작업 목록",
|
||||
@@ -43,7 +59,10 @@ internal static class AgentTranscriptDisplayCatalog
|
||||
if (!slashPrefix)
|
||||
return mapped;
|
||||
|
||||
return normalized.StartsWith('/') ? normalized : "/" + normalized.Replace(' ', '-');
|
||||
if (normalized.StartsWith('/'))
|
||||
return normalized;
|
||||
|
||||
return "/" + lowered.Replace('_', '-').Replace(' ', '-');
|
||||
}
|
||||
|
||||
public static string GetEventBadgeLabel(AgentEvent evt)
|
||||
@@ -59,22 +78,23 @@ internal static class AgentTranscriptDisplayCatalog
|
||||
|
||||
public static string GetTaskCategoryLabel(string? kind, string? title)
|
||||
{
|
||||
if (string.Equals(kind, "permission", System.StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(kind, "permission", StringComparison.OrdinalIgnoreCase))
|
||||
return "권한";
|
||||
if (string.Equals(kind, "queue", System.StringComparison.OrdinalIgnoreCase))
|
||||
return "큐";
|
||||
if (string.Equals(kind, "hook", System.StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(kind, "queue", StringComparison.OrdinalIgnoreCase))
|
||||
return "대기열";
|
||||
if (string.Equals(kind, "hook", StringComparison.OrdinalIgnoreCase))
|
||||
return "훅";
|
||||
if (string.Equals(kind, "subagent", System.StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(kind, "subagent", StringComparison.OrdinalIgnoreCase))
|
||||
return "에이전트";
|
||||
if (string.Equals(kind, "tool", System.StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(kind, "tool", StringComparison.OrdinalIgnoreCase))
|
||||
return GetToolCategoryLabel(title);
|
||||
|
||||
return "작업";
|
||||
}
|
||||
|
||||
public static string BuildEventSummary(AgentEvent evt, string displayName)
|
||||
{
|
||||
var summary = (evt.Summary ?? "").Trim();
|
||||
var summary = (evt.Summary ?? string.Empty).Trim();
|
||||
if (!string.IsNullOrWhiteSpace(summary))
|
||||
return summary;
|
||||
|
||||
@@ -83,9 +103,11 @@ internal static class AgentTranscriptDisplayCatalog
|
||||
AgentEventType.ToolCall => $"{displayName} 실행 준비",
|
||||
AgentEventType.ToolResult => evt.Success ? $"{displayName} 실행 완료" : $"{displayName} 실행 실패",
|
||||
AgentEventType.SkillCall => $"{displayName} 실행",
|
||||
AgentEventType.PermissionRequest => $"{displayName} 실행 전 사용자 확인 필요",
|
||||
AgentEventType.PermissionGranted => $"{displayName} 실행이 허용됨",
|
||||
AgentEventType.PermissionDenied => $"{displayName} 실행이 거부됨",
|
||||
AgentEventType.PermissionRequest => $"{displayName} 실행 전에 권한 확인이 필요합니다.",
|
||||
AgentEventType.PermissionGranted => $"{displayName} 실행 권한이 승인되었습니다.",
|
||||
AgentEventType.PermissionDenied => $"{displayName} 실행 권한이 거부되었습니다.",
|
||||
AgentEventType.Complete => "에이전트 작업이 완료되었습니다.",
|
||||
AgentEventType.Error => "에이전트 실행 중 오류가 발생했습니다.",
|
||||
_ => summary,
|
||||
};
|
||||
}
|
||||
@@ -97,14 +119,24 @@ 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" => "파일",
|
||||
"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" => "문서",
|
||||
"user_ask" => "질문",
|
||||
"suggest_actions" => "제안",
|
||||
"process" => "실행",
|
||||
"spawn_agent" or "wait_agents" => "에이전트",
|
||||
"file_read" or "file_write" or "file_edit" or "glob" or "grep" or "folder_map" or "file_watch" or "file_info" or "file_manage"
|
||||
=> "파일",
|
||||
"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"
|
||||
=> "문서",
|
||||
"user_ask"
|
||||
=> "질문",
|
||||
"suggest_actions"
|
||||
=> "제안",
|
||||
"process" or "bash" or "powershell"
|
||||
=> "명령",
|
||||
"spawn_agent" or "wait_agents"
|
||||
=> "에이전트",
|
||||
"web_fetch" or "http"
|
||||
=> "웹",
|
||||
_ => "도구",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
internal sealed record PermissionRequestPresentation(
|
||||
@@ -10,31 +12,52 @@ internal static class PermissionRequestPresentationCatalog
|
||||
{
|
||||
public static PermissionRequestPresentation Resolve(string? toolName, bool pending)
|
||||
{
|
||||
var tool = toolName?.Trim().ToLowerInvariant() ?? "";
|
||||
var tool = (toolName ?? string.Empty).Trim().ToLowerInvariant();
|
||||
|
||||
if (tool.Contains("process") || tool.Contains("bash") || tool.Contains("powershell"))
|
||||
if (tool.Contains("bash") || tool.Contains("powershell") || tool.Contains("process"))
|
||||
{
|
||||
return pending
|
||||
? new PermissionRequestPresentation("\uE756", "명령 권한 요청", "#FEF2F2", "#DC2626")
|
||||
: new PermissionRequestPresentation("\uE73E", "명령 권한 허용", "#ECFDF5", "#059669");
|
||||
? new PermissionRequestPresentation("\uE756", "명령 실행 권한 요청", "#FEF2F2", "#DC2626")
|
||||
: new PermissionRequestPresentation("\uE73E", "명령 실행 권한 승인", "#ECFDF5", "#059669");
|
||||
}
|
||||
|
||||
if (tool.Contains("web") || tool.Contains("fetch") || tool.Contains("http"))
|
||||
{
|
||||
return pending
|
||||
? new PermissionRequestPresentation("\uE774", "네트워크 권한 요청", "#FFF7ED", "#C2410C")
|
||||
: new PermissionRequestPresentation("\uE73E", "네트워크 권한 허용", "#ECFDF5", "#059669");
|
||||
? new PermissionRequestPresentation("\uE774", "웹 요청 권한 요청", "#FFF7ED", "#C2410C")
|
||||
: new PermissionRequestPresentation("\uE73E", "웹 요청 권한 승인", "#ECFDF5", "#059669");
|
||||
}
|
||||
|
||||
if (tool.Contains("file"))
|
||||
if (tool.Contains("skill"))
|
||||
{
|
||||
return pending
|
||||
? new PermissionRequestPresentation("\uE8A5", "파일 권한 요청", "#FFF7ED", "#C2410C")
|
||||
: new PermissionRequestPresentation("\uE73E", "파일 권한 허용", "#ECFDF5", "#059669");
|
||||
? new PermissionRequestPresentation("\uE8A5", "스킬 실행 권한 요청", "#F5F3FF", "#7C3AED")
|
||||
: new PermissionRequestPresentation("\uE73E", "스킬 실행 권한 승인", "#ECFDF5", "#059669");
|
||||
}
|
||||
|
||||
if (tool.Contains("ask"))
|
||||
{
|
||||
return pending
|
||||
? new PermissionRequestPresentation("\uE897", "의견 요청 권한 확인", "#EFF6FF", "#2563EB")
|
||||
: new PermissionRequestPresentation("\uE73E", "의견 요청 권한 승인", "#ECFDF5", "#059669");
|
||||
}
|
||||
|
||||
if (tool.Contains("file_edit") || tool.Contains("file_write") || tool.Contains("edit"))
|
||||
{
|
||||
return pending
|
||||
? new PermissionRequestPresentation("\uE70F", "파일 수정 권한 요청", "#FFF7ED", "#C2410C")
|
||||
: new PermissionRequestPresentation("\uE73E", "파일 수정 권한 승인", "#ECFDF5", "#059669");
|
||||
}
|
||||
|
||||
if (tool.Contains("file") || tool.Contains("glob") || tool.Contains("grep") || tool.Contains("folder"))
|
||||
{
|
||||
return pending
|
||||
? new PermissionRequestPresentation("\uE8A5", "파일 접근 권한 요청", "#FFF7ED", "#C2410C")
|
||||
: new PermissionRequestPresentation("\uE73E", "파일 접근 권한 승인", "#ECFDF5", "#059669");
|
||||
}
|
||||
|
||||
return pending
|
||||
? new PermissionRequestPresentation("\uE897", "권한 요청", "#FFF7ED", "#C2410C")
|
||||
: new PermissionRequestPresentation("\uE73E", "권한 허용", "#ECFDF5", "#059669");
|
||||
: new PermissionRequestPresentation("\uE73E", "권한 승인", "#ECFDF5", "#059669");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
internal sealed record ToolResultPresentation(
|
||||
@@ -11,27 +13,79 @@ internal static class ToolResultPresentationCatalog
|
||||
{
|
||||
public static ToolResultPresentation Resolve(AgentEvent evt, string fallbackLabel)
|
||||
{
|
||||
var summary = evt.Summary?.Trim() ?? "";
|
||||
var summary = (evt.Summary ?? string.Empty).Trim();
|
||||
var tool = (evt.ToolName ?? string.Empty).Trim().ToLowerInvariant();
|
||||
var baseLabel = string.IsNullOrWhiteSpace(fallbackLabel) ? "도구 결과" : fallbackLabel;
|
||||
|
||||
if (summary.Contains("취소", StringComparison.OrdinalIgnoreCase) ||
|
||||
summary.Contains("중단", StringComparison.OrdinalIgnoreCase) ||
|
||||
evt.Type == AgentEventType.StopRequested)
|
||||
{
|
||||
return new ToolResultPresentation("\uE711", "도구 취소", "#F8FAFC", "#475569", "cancel");
|
||||
return new ToolResultPresentation("\uE711", $"{baseLabel} 취소", "#F8FAFC", "#475569", "cancel");
|
||||
}
|
||||
|
||||
if (summary.Contains("거부", StringComparison.OrdinalIgnoreCase) ||
|
||||
summary.Contains("반려", StringComparison.OrdinalIgnoreCase) ||
|
||||
summary.Contains("권한 거부", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new ToolResultPresentation("\uE783", "도구 거부", "#FEF2F2", "#DC2626", "reject");
|
||||
return new ToolResultPresentation("\uE783", $"{baseLabel} 거부", "#FEF2F2", "#DC2626", "reject");
|
||||
}
|
||||
|
||||
if (!evt.Success || evt.Type == AgentEventType.Error)
|
||||
{
|
||||
return new ToolResultPresentation("\uE783", "도구 실패", "#FEF2F2", "#DC2626", "error");
|
||||
return new ToolResultPresentation(
|
||||
"\uE783",
|
||||
BuildFailureLabel(tool, baseLabel),
|
||||
"#FEF2F2",
|
||||
"#DC2626",
|
||||
"error");
|
||||
}
|
||||
|
||||
return new ToolResultPresentation("\uE73E", fallbackLabel, "#ECFDF5", "#16A34A", "success");
|
||||
return new ToolResultPresentation(
|
||||
"\uE73E",
|
||||
BuildSuccessLabel(tool, baseLabel),
|
||||
"#ECFDF5",
|
||||
"#16A34A",
|
||||
"success");
|
||||
}
|
||||
|
||||
private static string BuildSuccessLabel(string tool, string baseLabel)
|
||||
{
|
||||
if (tool.Contains("file"))
|
||||
return "파일 작업 완료";
|
||||
if (tool.Contains("build") || tool.Contains("test"))
|
||||
return "빌드/테스트 완료";
|
||||
if (tool.Contains("git") || tool.Contains("diff"))
|
||||
return "Git 작업 완료";
|
||||
if (tool.Contains("document") || tool.Contains("format") || tool.Contains("template"))
|
||||
return "문서 작업 완료";
|
||||
if (tool.Contains("skill"))
|
||||
return "스킬 실행 완료";
|
||||
if (tool.Contains("web") || tool.Contains("fetch") || tool.Contains("http"))
|
||||
return "웹 요청 완료";
|
||||
if (tool.Contains("process") || tool.Contains("bash") || tool.Contains("powershell"))
|
||||
return "명령 실행 완료";
|
||||
|
||||
return baseLabel;
|
||||
}
|
||||
|
||||
private static string BuildFailureLabel(string tool, string baseLabel)
|
||||
{
|
||||
if (tool.Contains("file"))
|
||||
return "파일 작업 실패";
|
||||
if (tool.Contains("build") || tool.Contains("test"))
|
||||
return "빌드/테스트 실패";
|
||||
if (tool.Contains("git") || tool.Contains("diff"))
|
||||
return "Git 작업 실패";
|
||||
if (tool.Contains("document") || tool.Contains("format") || tool.Contains("template"))
|
||||
return "문서 작업 실패";
|
||||
if (tool.Contains("skill"))
|
||||
return "스킬 실행 실패";
|
||||
if (tool.Contains("web") || tool.Contains("fetch") || tool.Contains("http"))
|
||||
return "웹 요청 실패";
|
||||
if (tool.Contains("process") || tool.Contains("bash") || tool.Contains("powershell"))
|
||||
return "명령 실행 실패";
|
||||
|
||||
return $"{baseLabel} 실패";
|
||||
}
|
||||
}
|
||||
|
||||
421
src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs
Normal file
421
src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs
Normal file
@@ -0,0 +1,421 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private Border CreateCompactEventPill(string summary, Brush primaryText, Brush secondaryText, Brush hintBg, Brush borderBrush, Brush accentBrush)
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Padding = new Thickness(6, 2, 6, 2),
|
||||
Margin = new Thickness(8, 2, 220, 2),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
Child = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = "\uE9CE",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 8,
|
||||
Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = summary,
|
||||
FontSize = 8.75,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(4, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void AddAgentEventBanner(AgentEvent evt)
|
||||
{
|
||||
var logLevel = _settings.Settings.Llm.AgentLogLevel;
|
||||
|
||||
if (evt.Type == AgentEventType.Planning && evt.Steps is { Count: > 0 })
|
||||
{
|
||||
var compactPrimaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var compactSecondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var compactHintBg = TryFindResource("HintBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
var compactBorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var compactAccentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var summary = !string.IsNullOrWhiteSpace(evt.Summary)
|
||||
? evt.Summary!
|
||||
: $"계획 {evt.Steps.Count}단계";
|
||||
var pill = CreateCompactEventPill(summary, compactPrimaryText, compactSecondaryText, compactHintBg, compactBorderBrush, compactAccentBrush);
|
||||
pill.Opacity = 0;
|
||||
pill.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(160)));
|
||||
MessagePanel.Children.Add(pill);
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.Type == AgentEventType.StepStart && evt.StepTotal > 0)
|
||||
{
|
||||
UpdateProgressBar(evt);
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.Type == AgentEventType.Thinking &&
|
||||
ContainsAny(evt.Summary ?? "", "컨텍스트 압축", "microcompact", "session memory", "compact"))
|
||||
{
|
||||
var compactPrimaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var compactSecondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var compactHintBg = TryFindResource("HintBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
var compactBorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var compactAccentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var pill = CreateCompactEventPill(string.IsNullOrWhiteSpace(evt.Summary) ? "컨텍스트 압축" : evt.Summary, compactPrimaryText, compactSecondaryText, compactHintBg, compactBorderBrush, compactAccentBrush);
|
||||
pill.Opacity = 0;
|
||||
pill.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(160)));
|
||||
MessagePanel.Children.Add(pill);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(logLevel, "debug", StringComparison.OrdinalIgnoreCase)
|
||||
&& evt.Type is AgentEventType.Paused or AgentEventType.Resumed)
|
||||
return;
|
||||
|
||||
if (!string.Equals(logLevel, "debug", StringComparison.OrdinalIgnoreCase)
|
||||
&& evt.Type == AgentEventType.ToolCall)
|
||||
return;
|
||||
|
||||
var isTotalStats = evt.Type == AgentEventType.StepDone && evt.ToolName == "total_stats";
|
||||
var transcriptBadgeLabel = GetTranscriptBadgeLabel(evt);
|
||||
var permissionPresentation = evt.Type switch
|
||||
{
|
||||
AgentEventType.PermissionRequest => PermissionRequestPresentationCatalog.Resolve(evt.ToolName, pending: true),
|
||||
AgentEventType.PermissionGranted => PermissionRequestPresentationCatalog.Resolve(evt.ToolName, pending: false),
|
||||
_ => null
|
||||
};
|
||||
var toolResultPresentation = evt.Type == AgentEventType.ToolResult
|
||||
? ToolResultPresentationCatalog.Resolve(evt, transcriptBadgeLabel)
|
||||
: null;
|
||||
|
||||
var (icon, label, bgHex, fgHex) = isTotalStats
|
||||
? ("\uE9D2", "전체 통계", "#F3EEFF", "#7C3AED")
|
||||
: evt.Type switch
|
||||
{
|
||||
AgentEventType.Thinking => ("\uE8BD", "분석 중", "#F0F0FF", "#6B7BC4"),
|
||||
AgentEventType.PermissionRequest => (permissionPresentation!.Icon, permissionPresentation.Label, permissionPresentation.BackgroundHex, permissionPresentation.ForegroundHex),
|
||||
AgentEventType.PermissionGranted => (permissionPresentation!.Icon, permissionPresentation.Label, permissionPresentation.BackgroundHex, permissionPresentation.ForegroundHex),
|
||||
AgentEventType.PermissionDenied => ("\uE783", "권한 거부", "#FEF2F2", "#DC2626"),
|
||||
AgentEventType.Decision => GetDecisionBadgeMeta(evt.Summary),
|
||||
AgentEventType.ToolCall => ("\uE8A7", transcriptBadgeLabel, "#EEF6FF", "#3B82F6"),
|
||||
AgentEventType.ToolResult => (toolResultPresentation!.Icon, toolResultPresentation.Label, toolResultPresentation.BackgroundHex, toolResultPresentation.ForegroundHex),
|
||||
AgentEventType.SkillCall => ("\uE8A5", transcriptBadgeLabel, "#FFF7ED", "#EA580C"),
|
||||
AgentEventType.Error => ("\uE783", "오류", "#FEF2F2", "#DC2626"),
|
||||
AgentEventType.Complete => ("\uE930", "완료", "#F0FFF4", "#15803D"),
|
||||
AgentEventType.StepDone => ("\uE73E", "단계 완료", "#EEF9EE", "#16A34A"),
|
||||
AgentEventType.Paused => ("\uE769", "일시정지", "#FFFBEB", "#D97706"),
|
||||
AgentEventType.Resumed => ("\uE768", "재개", "#ECFDF5", "#059669"),
|
||||
_ => ("\uE946", "에이전트", "#F5F5F5", "#6B7280"),
|
||||
};
|
||||
var itemDisplayName = evt.Type == AgentEventType.SkillCall
|
||||
? GetAgentItemDisplayName(evt.ToolName, slashPrefix: true)
|
||||
: GetAgentItemDisplayName(evt.ToolName);
|
||||
var eventSummaryText = BuildAgentEventSummaryText(evt, itemDisplayName);
|
||||
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray;
|
||||
var hintBg = TryFindResource("HintBackground") as Brush ?? BrushFromHex("#F8FAFC");
|
||||
var borderColor = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E2E8F0");
|
||||
var accentBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(fgHex));
|
||||
|
||||
var banner = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
CornerRadius = new CornerRadius(0),
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(12, 0, 12, 1),
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(evt.RunId))
|
||||
_runBannerAnchors[evt.RunId] = banner;
|
||||
|
||||
var sp = new StackPanel();
|
||||
|
||||
var headerGrid = new Grid();
|
||||
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var headerLeft = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
headerLeft.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 8.25,
|
||||
Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 3, 0),
|
||||
});
|
||||
headerLeft.Children.Add(new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 8.25,
|
||||
FontWeight = FontWeights.Medium,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
if (IsTranscriptToolLikeEvent(evt) && !string.IsNullOrWhiteSpace(evt.ToolName))
|
||||
{
|
||||
headerLeft.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $" · {itemDisplayName}",
|
||||
FontSize = 8.25,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
}
|
||||
Grid.SetColumn(headerLeft, 0);
|
||||
|
||||
var headerRight = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
if (logLevel != "simple" && evt.ElapsedMs > 0)
|
||||
{
|
||||
headerRight.Children.Add(new TextBlock
|
||||
{
|
||||
Text = evt.ElapsedMs < 1000 ? $"{evt.ElapsedMs}ms" : $"{evt.ElapsedMs / 1000.0:F1}s",
|
||||
FontSize = 7.5,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(3, 0, 0, 0),
|
||||
});
|
||||
}
|
||||
if (logLevel != "simple" && (evt.InputTokens > 0 || evt.OutputTokens > 0))
|
||||
{
|
||||
var tokenText = evt.InputTokens > 0 && evt.OutputTokens > 0
|
||||
? $"{evt.InputTokens}→{evt.OutputTokens}t"
|
||||
: evt.InputTokens > 0 ? $"↑{evt.InputTokens}t" : $"↓{evt.OutputTokens}t";
|
||||
headerRight.Children.Add(new Border
|
||||
{
|
||||
Background = hintBg,
|
||||
BorderBrush = borderColor,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Padding = new Thickness(3.5, 1, 3.5, 1),
|
||||
Margin = new Thickness(3, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = tokenText,
|
||||
FontSize = 7.25,
|
||||
Foreground = secondaryText,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
},
|
||||
});
|
||||
}
|
||||
Grid.SetColumn(headerRight, 1);
|
||||
|
||||
headerGrid.Children.Add(headerLeft);
|
||||
headerGrid.Children.Add(headerRight);
|
||||
sp.Children.Add(headerGrid);
|
||||
|
||||
if (logLevel == "simple")
|
||||
{
|
||||
if (!string.IsNullOrEmpty(eventSummaryText))
|
||||
{
|
||||
var shortSummary = eventSummaryText.Length > 100
|
||||
? eventSummaryText[..100] + "…"
|
||||
: eventSummaryText;
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = shortSummary,
|
||||
FontSize = 8.4,
|
||||
Foreground = secondaryText,
|
||||
TextWrapping = TextWrapping.NoWrap,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
Margin = new Thickness(11, 1, 0, 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(eventSummaryText))
|
||||
{
|
||||
var summaryText = eventSummaryText.Length > 92 ? eventSummaryText[..92] + "…" : eventSummaryText;
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = summaryText,
|
||||
FontSize = 8.4,
|
||||
Foreground = secondaryText,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(11, 1, 0, 0),
|
||||
});
|
||||
}
|
||||
|
||||
var reviewChipRow = BuildReviewSignalChipRow(
|
||||
kind: null,
|
||||
toolName: evt.ToolName,
|
||||
title: label,
|
||||
summary: evt.Summary);
|
||||
if (reviewChipRow != null && string.Equals(logLevel, "debug", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
reviewChipRow.Margin = new Thickness(12, 2, 0, 0);
|
||||
sp.Children.Add(reviewChipRow);
|
||||
}
|
||||
|
||||
if (logLevel == "debug" && !string.IsNullOrEmpty(evt.ToolInput))
|
||||
{
|
||||
sp.Children.Add(new Border
|
||||
{
|
||||
Background = hintBg,
|
||||
BorderBrush = borderColor,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(5, 3, 5, 3),
|
||||
Margin = new Thickness(12, 2, 0, 0),
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = evt.ToolInput.Length > 240 ? evt.ToolInput[..240] + "…" : evt.ToolInput,
|
||||
FontSize = 8.5,
|
||||
Foreground = secondaryText,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(evt.FilePath))
|
||||
{
|
||||
var fileName = System.IO.Path.GetFileName(evt.FilePath);
|
||||
var dirName = System.IO.Path.GetDirectoryName(evt.FilePath) ?? "";
|
||||
|
||||
if (!string.Equals(logLevel, "debug", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var compactPathRow = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Margin = new Thickness(12, 1.5, 0, 0),
|
||||
ToolTip = evt.FilePath,
|
||||
};
|
||||
compactPathRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE8B7",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 8,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 3, 0),
|
||||
});
|
||||
compactPathRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(fileName) ? evt.FilePath : fileName,
|
||||
FontSize = 8.5,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
});
|
||||
sp.Children.Add(compactPathRow);
|
||||
}
|
||||
else
|
||||
{
|
||||
var pathBorder = new Border
|
||||
{
|
||||
Background = hintBg,
|
||||
BorderBrush = borderColor,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(7, 4, 7, 4),
|
||||
Margin = new Thickness(12, 2, 0, 0),
|
||||
};
|
||||
|
||||
var pathGrid = new Grid();
|
||||
pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var left = new StackPanel { Orientation = Orientation.Vertical };
|
||||
|
||||
var topRow = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
topRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE8B7",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 10,
|
||||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CA3AF")),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
});
|
||||
topRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(fileName) ? evt.FilePath : fileName,
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#374151")),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
});
|
||||
left.Children.Add(topRow);
|
||||
if (!string.IsNullOrWhiteSpace(dirName))
|
||||
{
|
||||
left.Children.Add(new TextBlock
|
||||
{
|
||||
Text = dirName,
|
||||
FontSize = 9,
|
||||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CA3AF")),
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
});
|
||||
}
|
||||
Grid.SetColumn(left, 0);
|
||||
pathGrid.Children.Add(left);
|
||||
|
||||
var quickActions = BuildFileQuickActions(evt.FilePath);
|
||||
Grid.SetColumn(quickActions, 1);
|
||||
pathGrid.Children.Add(quickActions);
|
||||
|
||||
pathBorder.Child = pathGrid;
|
||||
sp.Children.Add(pathBorder);
|
||||
}
|
||||
}
|
||||
|
||||
banner.Child = sp;
|
||||
|
||||
if (isTotalStats)
|
||||
{
|
||||
banner.Cursor = Cursors.Hand;
|
||||
banner.ToolTip = "클릭하여 병목 분석 보기";
|
||||
banner.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
OpenWorkflowAnalyzerIfEnabled();
|
||||
_analyzerWindow?.SwitchToBottleneckTab();
|
||||
_analyzerWindow?.Activate();
|
||||
};
|
||||
}
|
||||
|
||||
banner.Opacity = 0;
|
||||
banner.BeginAnimation(UIElement.OpacityProperty,
|
||||
new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200)));
|
||||
|
||||
MessagePanel.Children.Add(banner);
|
||||
}
|
||||
|
||||
private static (string icon, string label, string bgHex, string fgHex) GetDecisionBadgeMeta(string? summary)
|
||||
{
|
||||
if (IsDecisionApproved(summary))
|
||||
return ("\uE73E", "계획 승인", "#ECFDF5", "#059669");
|
||||
if (IsDecisionRejected(summary))
|
||||
return ("\uE783", "계획 반려", "#FEF2F2", "#DC2626");
|
||||
return ("\uE70F", "계획 확인", "#FFF7ED", "#C2410C");
|
||||
}
|
||||
}
|
||||
@@ -4020,43 +4020,6 @@ public partial class ChatWindow : Window
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private Border CreateCompactEventPill(string summary, Brush primaryText, Brush secondaryText, Brush hintBg, Brush borderBrush, Brush accentBrush)
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Padding = new Thickness(6, 2, 6, 2),
|
||||
Margin = new Thickness(8, 2, 220, 2),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
Child = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = "\uE9CE",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 8,
|
||||
Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = summary,
|
||||
FontSize = 8.75,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(4, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void AddMessageBubble(string role, string content, bool animate = true, ChatMessage? message = null)
|
||||
{
|
||||
var isUser = role == "user";
|
||||
@@ -10008,387 +9971,6 @@ public partial class ChatWindow : Window
|
||||
return row.Children.Count == 0 ? null : row;
|
||||
}
|
||||
|
||||
private void AddAgentEventBanner(AgentEvent evt)
|
||||
{
|
||||
var logLevel = _settings.Settings.Llm.AgentLogLevel;
|
||||
|
||||
// Planning 이벤트는 claw-code 기준으로 기본 transcript에 요약 pill만 남깁니다.
|
||||
if (evt.Type == AgentEventType.Planning && evt.Steps is { Count: > 0 })
|
||||
{
|
||||
var compactPrimaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var compactSecondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var compactHintBg = TryFindResource("HintBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
var compactBorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var compactAccentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var summary = !string.IsNullOrWhiteSpace(evt.Summary)
|
||||
? evt.Summary!
|
||||
: $"계획 {evt.Steps.Count}단계";
|
||||
var pill = CreateCompactEventPill(summary, compactPrimaryText, compactSecondaryText, compactHintBg, compactBorderBrush, compactAccentBrush);
|
||||
pill.Opacity = 0;
|
||||
pill.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(160)));
|
||||
MessagePanel.Children.Add(pill);
|
||||
return;
|
||||
}
|
||||
|
||||
// StepStart 이벤트는 진행률 바 업데이트
|
||||
if (evt.Type == AgentEventType.StepStart && evt.StepTotal > 0)
|
||||
{
|
||||
UpdateProgressBar(evt);
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.Type == AgentEventType.Thinking &&
|
||||
ContainsAny(evt.Summary ?? "", "컨텍스트 압축", "microcompact", "session memory", "compact"))
|
||||
{
|
||||
var compactPrimaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var compactSecondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var compactHintBg = TryFindResource("HintBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
var compactBorderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var compactAccentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var pill = CreateCompactEventPill(string.IsNullOrWhiteSpace(evt.Summary) ? "컨텍스트 압축" : evt.Summary, compactPrimaryText, compactSecondaryText, compactHintBg, compactBorderBrush, compactAccentBrush);
|
||||
pill.Opacity = 0;
|
||||
pill.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(160)));
|
||||
MessagePanel.Children.Add(pill);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(logLevel, "debug", StringComparison.OrdinalIgnoreCase)
|
||||
&& evt.Type is AgentEventType.Paused or AgentEventType.Resumed)
|
||||
return;
|
||||
|
||||
// 기본 로그에서는 중간 ToolCall을 숨겨 결과/오류 중심으로 정리합니다.
|
||||
if (!string.Equals(logLevel, "debug", StringComparison.OrdinalIgnoreCase)
|
||||
&& evt.Type == AgentEventType.ToolCall)
|
||||
return;
|
||||
|
||||
// 전체 통계 이벤트는 별도 색상 (보라색 계열)
|
||||
var isTotalStats = evt.Type == AgentEventType.StepDone && evt.ToolName == "total_stats";
|
||||
var transcriptBadgeLabel = GetTranscriptBadgeLabel(evt);
|
||||
var permissionPresentation = evt.Type switch
|
||||
{
|
||||
AgentEventType.PermissionRequest => PermissionRequestPresentationCatalog.Resolve(evt.ToolName, pending: true),
|
||||
AgentEventType.PermissionGranted => PermissionRequestPresentationCatalog.Resolve(evt.ToolName, pending: false),
|
||||
_ => null
|
||||
};
|
||||
var toolResultPresentation = evt.Type == AgentEventType.ToolResult
|
||||
? ToolResultPresentationCatalog.Resolve(evt, transcriptBadgeLabel)
|
||||
: null;
|
||||
|
||||
var (icon, label, bgHex, fgHex) = isTotalStats
|
||||
? ("\uE9D2", "전체 통계", "#F3EEFF", "#7C3AED")
|
||||
: evt.Type switch
|
||||
{
|
||||
AgentEventType.Thinking => ("\uE8BD", "분석 중", "#F0F0FF", "#6B7BC4"),
|
||||
AgentEventType.PermissionRequest => (permissionPresentation!.Icon, permissionPresentation.Label, permissionPresentation.BackgroundHex, permissionPresentation.ForegroundHex),
|
||||
AgentEventType.PermissionGranted => (permissionPresentation!.Icon, permissionPresentation.Label, permissionPresentation.BackgroundHex, permissionPresentation.ForegroundHex),
|
||||
AgentEventType.PermissionDenied => ("\uE783", "권한 거부", "#FEF2F2", "#DC2626"),
|
||||
AgentEventType.Decision => GetDecisionBadgeMeta(evt.Summary),
|
||||
AgentEventType.ToolCall => ("\uE8A7", transcriptBadgeLabel, "#EEF6FF", "#3B82F6"),
|
||||
AgentEventType.ToolResult => (toolResultPresentation!.Icon, toolResultPresentation.Label, toolResultPresentation.BackgroundHex, toolResultPresentation.ForegroundHex),
|
||||
AgentEventType.SkillCall => ("\uE8A5", transcriptBadgeLabel, "#FFF7ED", "#EA580C"),
|
||||
AgentEventType.Error => ("\uE783", "오류", "#FEF2F2", "#DC2626"),
|
||||
AgentEventType.Complete => ("\uE930", "완료", "#F0FFF4", "#15803D"),
|
||||
AgentEventType.StepDone => ("\uE73E", "단계 완료", "#EEF9EE", "#16A34A"),
|
||||
AgentEventType.Paused => ("\uE769", "일시정지", "#FFFBEB", "#D97706"),
|
||||
AgentEventType.Resumed => ("\uE768", "재개", "#ECFDF5", "#059669"),
|
||||
_ => ("\uE946", "에이전트", "#F5F5F5", "#6B7280"),
|
||||
};
|
||||
var itemDisplayName = evt.Type == AgentEventType.SkillCall
|
||||
? GetAgentItemDisplayName(evt.ToolName, slashPrefix: true)
|
||||
: GetAgentItemDisplayName(evt.ToolName);
|
||||
var eventSummaryText = BuildAgentEventSummaryText(evt, itemDisplayName);
|
||||
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray;
|
||||
var hintBg = TryFindResource("HintBackground") as Brush ?? BrushFromHex("#F8FAFC");
|
||||
var borderColor = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E2E8F0");
|
||||
var accentBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(fgHex));
|
||||
|
||||
var banner = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
CornerRadius = new CornerRadius(0),
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(12, 0, 12, 1),
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(evt.RunId))
|
||||
_runBannerAnchors[evt.RunId] = banner;
|
||||
|
||||
var sp = new StackPanel();
|
||||
|
||||
// 헤더: 얇은 실행 줄 형태
|
||||
var headerGrid = new Grid();
|
||||
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
// 좌측: 아이콘 + 라벨
|
||||
var headerLeft = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
headerLeft.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 8.25,
|
||||
Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 3, 0),
|
||||
});
|
||||
headerLeft.Children.Add(new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 8.25,
|
||||
FontWeight = FontWeights.Medium,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
if (IsTranscriptToolLikeEvent(evt) && !string.IsNullOrWhiteSpace(evt.ToolName))
|
||||
{
|
||||
headerLeft.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $" · {itemDisplayName}",
|
||||
FontSize = 8.25,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
}
|
||||
Grid.SetColumn(headerLeft, 0);
|
||||
|
||||
// 우측: 소요 시간 + 토큰 배지 (항상 우측 끝에 고정)
|
||||
var headerRight = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
if (logLevel != "simple" && evt.ElapsedMs > 0)
|
||||
{
|
||||
headerRight.Children.Add(new TextBlock
|
||||
{
|
||||
Text = evt.ElapsedMs < 1000 ? $"{evt.ElapsedMs}ms" : $"{evt.ElapsedMs / 1000.0:F1}s",
|
||||
FontSize = 7.5,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(3, 0, 0, 0),
|
||||
});
|
||||
}
|
||||
if (logLevel != "simple" && (evt.InputTokens > 0 || evt.OutputTokens > 0))
|
||||
{
|
||||
var tokenText = evt.InputTokens > 0 && evt.OutputTokens > 0
|
||||
? $"{evt.InputTokens}→{evt.OutputTokens}t"
|
||||
: evt.InputTokens > 0 ? $"↑{evt.InputTokens}t" : $"↓{evt.OutputTokens}t";
|
||||
headerRight.Children.Add(new Border
|
||||
{
|
||||
Background = hintBg,
|
||||
BorderBrush = borderColor,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(999),
|
||||
Padding = new Thickness(3.5, 1, 3.5, 1),
|
||||
Margin = new Thickness(3, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = tokenText,
|
||||
FontSize = 7.25,
|
||||
Foreground = secondaryText,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
},
|
||||
});
|
||||
}
|
||||
Grid.SetColumn(headerRight, 1);
|
||||
|
||||
headerGrid.Children.Add(headerLeft);
|
||||
headerGrid.Children.Add(headerRight);
|
||||
|
||||
// header 변수를 headerLeft로 설정 (이후 expandIcon 추가 시 사용)
|
||||
var header = headerLeft;
|
||||
|
||||
sp.Children.Add(headerGrid);
|
||||
|
||||
// simple 모드: 요약 한 줄만 표시 (본문 로그)
|
||||
if (logLevel == "simple")
|
||||
{
|
||||
if (!string.IsNullOrEmpty(eventSummaryText))
|
||||
{
|
||||
var shortSummary = eventSummaryText.Length > 100
|
||||
? eventSummaryText[..100] + "…"
|
||||
: eventSummaryText;
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = shortSummary,
|
||||
FontSize = 8.4,
|
||||
Foreground = secondaryText,
|
||||
TextWrapping = TextWrapping.NoWrap,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
Margin = new Thickness(11, 1, 0, 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
// detailed/debug 모드: 실행 줄 아래에 얕은 설명만 표시
|
||||
else if (!string.IsNullOrEmpty(eventSummaryText))
|
||||
{
|
||||
var summaryText = eventSummaryText.Length > 92 ? eventSummaryText[..92] + "…" : eventSummaryText;
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = summaryText,
|
||||
FontSize = 8.4,
|
||||
Foreground = secondaryText,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(11, 1, 0, 0),
|
||||
});
|
||||
}
|
||||
|
||||
var reviewChipRow = BuildReviewSignalChipRow(
|
||||
kind: null,
|
||||
toolName: evt.ToolName,
|
||||
title: label,
|
||||
summary: evt.Summary);
|
||||
if (reviewChipRow != null && string.Equals(logLevel, "debug", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
reviewChipRow.Margin = new Thickness(12, 2, 0, 0);
|
||||
sp.Children.Add(reviewChipRow);
|
||||
}
|
||||
|
||||
// debug 모드: ToolInput 파라미터 표시
|
||||
if (logLevel == "debug" && !string.IsNullOrEmpty(evt.ToolInput))
|
||||
{
|
||||
sp.Children.Add(new Border
|
||||
{
|
||||
Background = hintBg,
|
||||
BorderBrush = borderColor,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(5, 3, 5, 3),
|
||||
Margin = new Thickness(12, 2, 0, 0),
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = evt.ToolInput.Length > 240 ? evt.ToolInput[..240] + "…" : evt.ToolInput,
|
||||
FontSize = 8.5,
|
||||
Foreground = secondaryText,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 파일 경로 표시는 debug에서만 카드형으로 노출하고,
|
||||
// 일반 로그에서는 파일명 한 줄만 보여 본문 침범을 줄입니다.
|
||||
if (!string.IsNullOrEmpty(evt.FilePath))
|
||||
{
|
||||
var fileName = System.IO.Path.GetFileName(evt.FilePath);
|
||||
var dirName = System.IO.Path.GetDirectoryName(evt.FilePath) ?? "";
|
||||
|
||||
if (!string.Equals(logLevel, "debug", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var compactPathRow = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Margin = new Thickness(12, 1.5, 0, 0),
|
||||
ToolTip = evt.FilePath,
|
||||
};
|
||||
compactPathRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE8B7",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 8,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 3, 0),
|
||||
});
|
||||
compactPathRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(fileName) ? evt.FilePath : fileName,
|
||||
FontSize = 8.5,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
});
|
||||
sp.Children.Add(compactPathRow);
|
||||
}
|
||||
else
|
||||
{
|
||||
var pathBorder = new Border
|
||||
{
|
||||
Background = hintBg,
|
||||
BorderBrush = borderColor,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(7, 4, 7, 4),
|
||||
Margin = new Thickness(12, 2, 0, 0),
|
||||
};
|
||||
|
||||
var pathGrid = new Grid();
|
||||
pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var left = new StackPanel { Orientation = Orientation.Vertical };
|
||||
|
||||
var topRow = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
topRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE8B7",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 10,
|
||||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CA3AF")),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
});
|
||||
topRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(fileName) ? evt.FilePath : fileName,
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#374151")),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
});
|
||||
left.Children.Add(topRow);
|
||||
if (!string.IsNullOrWhiteSpace(dirName))
|
||||
{
|
||||
left.Children.Add(new TextBlock
|
||||
{
|
||||
Text = dirName,
|
||||
FontSize = 9,
|
||||
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CA3AF")),
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
});
|
||||
}
|
||||
Grid.SetColumn(left, 0);
|
||||
pathGrid.Children.Add(left);
|
||||
|
||||
var quickActions = BuildFileQuickActions(evt.FilePath);
|
||||
Grid.SetColumn(quickActions, 1);
|
||||
pathGrid.Children.Add(quickActions);
|
||||
|
||||
pathBorder.Child = pathGrid;
|
||||
sp.Children.Add(pathBorder);
|
||||
}
|
||||
}
|
||||
|
||||
banner.Child = sp;
|
||||
|
||||
// Total Stats 배너 클릭 → 워크플로우 분석기 병목 분석 탭 열기
|
||||
if (isTotalStats)
|
||||
{
|
||||
banner.Cursor = Cursors.Hand;
|
||||
banner.ToolTip = "클릭하여 병목 분석 보기";
|
||||
banner.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
OpenWorkflowAnalyzerIfEnabled();
|
||||
_analyzerWindow?.SwitchToBottleneckTab();
|
||||
_analyzerWindow?.Activate();
|
||||
};
|
||||
}
|
||||
|
||||
// 페이드인 애니메이션
|
||||
banner.Opacity = 0;
|
||||
banner.BeginAnimation(UIElement.OpacityProperty,
|
||||
new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200)));
|
||||
|
||||
MessagePanel.Children.Add(banner);
|
||||
}
|
||||
|
||||
/// <summary>파일 빠른 작업 버튼 패널을 생성합니다.</summary>
|
||||
private StackPanel BuildFileQuickActions(string filePath)
|
||||
{
|
||||
@@ -18236,15 +17818,6 @@ public partial class ChatWindow : Window
|
||||
if (spinning) StartStatusAnimation();
|
||||
}
|
||||
|
||||
private static (string icon, string label, string bgHex, string fgHex) GetDecisionBadgeMeta(string? summary)
|
||||
{
|
||||
if (IsDecisionApproved(summary))
|
||||
return ("\uE73E", "계획 승인", "#ECFDF5", "#059669");
|
||||
if (IsDecisionRejected(summary))
|
||||
return ("\uE783", "계획 반려", "#FEF2F2", "#DC2626");
|
||||
return ("\uE70F", "계획 확인", "#FFF7ED", "#C2410C");
|
||||
}
|
||||
|
||||
private static bool IsDecisionPending(string? summary)
|
||||
{
|
||||
var text = summary?.Trim() ?? "";
|
||||
|
||||
Reference in New Issue
Block a user