AX Agent transcript 권한·도구 결과 표현 정교화 및 이벤트 렌더 분리
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:
2026-04-06 06:59:22 +09:00
parent f9d18fba08
commit 95e40df354
7 changed files with 581 additions and 470 deletions

View File

@@ -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"
=> "웹",
_ => "도구",
};
}

View File

@@ -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");
}
}

View File

@@ -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} 실패";
}
}

View 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");
}
}

View File

@@ -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() ?? "";