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:
@@ -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