diff --git a/README.md b/README.md index c300471..42d05a1 100644 --- a/README.md +++ b/README.md @@ -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에서 관리한다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 0b713fa..689818a 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -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 확장을 더 쉽게 할 수 있는 구조를 마련했다. diff --git a/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs b/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs index 8e3d19c..0e3e905 100644 --- a/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs +++ b/src/AxCopilot/Services/Agent/AgentTranscriptDisplayCatalog.cs @@ -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" + => "웹", _ => "도구", }; } diff --git a/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs b/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs index c463244..57edc5a 100644 --- a/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs +++ b/src/AxCopilot/Services/Agent/PermissionRequestPresentationCatalog.cs @@ -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"); } } diff --git a/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs b/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs index 8d21551..64f7173 100644 --- a/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs +++ b/src/AxCopilot/Services/Agent/ToolResultPresentationCatalog.cs @@ -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} 실패"; } } diff --git a/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs b/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs new file mode 100644 index 0000000..d764ffc --- /dev/null +++ b/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs @@ -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"); + } +} diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 5a4a4ce..c4834b5 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -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); - } - /// 파일 빠른 작업 버튼 패널을 생성합니다. 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() ?? "";