Files
AX-Copilot-Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs
lacvet ec0ed7fb1c
Some checks failed
Release Gate / gate (push) Has been cancelled
권한·도구 결과 카드 callout 구조 도입
권한 요청과 도구 결과 transcript 안내를 단순 보조 문구에서 callout 카드 구조로 정리했습니다.

권한 요청은 확인 포인트, 도구 결과는 다음 권장 작업으로 보여주도록 바꿔 카드 성격 차이를 더 분명하게 만들었습니다.

README와 DEVELOPMENT 문서를 갱신했고 dotnet build 기준 경고 0 / 오류 0을 확인했습니다.
2026-04-06 13:43:19 +09:00

692 lines
30 KiB
C#

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 Border CreateAgentMetaChip(string text, string icon, Brush foreground, Brush background, Brush borderBrush)
{
return new Border
{
Background = background,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(999),
Padding = new Thickness(5, 1.5, 5, 1.5),
Margin = new Thickness(0, 0, 4, 0),
Child = new StackPanel
{
Orientation = Orientation.Horizontal,
Children =
{
new TextBlock
{
Text = icon,
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 7.5,
Foreground = foreground,
VerticalAlignment = VerticalAlignment.Center,
},
new TextBlock
{
Text = text,
FontSize = 7.75,
Foreground = foreground,
Margin = new Thickness(3, 0, 0, 0),
VerticalAlignment = VerticalAlignment.Center,
}
}
}
};
}
private Border CreateAgentMetaCallout(string title, string body, Brush titleBrush, Brush background, Brush borderBrush)
{
return new Border
{
Background = background,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(7, 5, 7, 5),
Margin = new Thickness(11, 2, 0, 0),
Child = new StackPanel
{
Children =
{
new TextBlock
{
Text = title,
FontSize = 7.75,
FontWeight = FontWeights.SemiBold,
Foreground = titleBrush,
},
new TextBlock
{
Text = body,
FontSize = 8.1,
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.DimGray,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 1, 0, 0),
}
}
}
};
}
private static string GetPermissionKindLabel(string kind)
{
return kind switch
{
"bash" => "Bash",
"powershell" => "PowerShell",
"command" => "명령 실행",
"web_fetch" => "웹 요청",
"mcp" => "MCP",
"skill" => "스킬",
"question" => "의견 요청",
"file_edit" => "파일 수정",
"file_write" => "파일 쓰기",
"git" => "Git",
"document" => "문서 작업",
"filesystem" => "파일 접근",
_ => "권한 요청",
};
}
private static string GetToolResultKindLabel(string kind)
{
return kind switch
{
"file_edit" => "파일 수정",
"file_write" => "파일 쓰기",
"filesystem" => "파일 탐색",
"file" => "파일 작업",
"build_test" => "빌드/테스트",
"git" => "Git",
"document" => "문서",
"skill" => "스킬",
"mcp" => "MCP",
"question" => "의견 요청",
"web" => "웹 요청",
"command" => "명령 실행",
_ => "도구 결과",
};
}
private void AppendAgentEventPresentationMeta(
StackPanel stack,
AgentEvent evt,
PermissionRequestPresentation? permissionPresentation,
ToolResultPresentation? toolResultPresentation,
Brush secondaryText,
Brush hintBg,
Brush borderColor)
{
string? guidance = null;
var chipRow = new WrapPanel
{
Margin = new Thickness(11, 2, 0, 0),
Orientation = Orientation.Horizontal,
};
if (permissionPresentation != null)
{
guidance = permissionPresentation.ActionHint;
chipRow.Children.Add(CreateAgentMetaChip(
GetPermissionKindLabel(permissionPresentation.Kind),
"\uE8A5",
BrushFromHex("#475569"),
BrushFromHex("#F8FAFC"),
BrushFromHex("#E2E8F0")));
if (permissionPresentation.RequiresPreview)
chipRow.Children.Add(CreateAgentMetaChip(
"미리보기 권장",
"\uE8A7",
BrushFromHex("#1D4ED8"),
BrushFromHex("#EFF6FF"),
BrushFromHex("#BFDBFE")));
if (evt.Type == AgentEventType.PermissionRequest)
{
if (string.Equals(permissionPresentation.Severity, "high", StringComparison.OrdinalIgnoreCase))
chipRow.Children.Add(CreateAgentMetaChip(
"주의 필요",
"\uE814",
BrushFromHex("#B91C1C"),
BrushFromHex("#FEF2F2"),
BrushFromHex("#FECACA")));
else if (string.Equals(permissionPresentation.Severity, "medium", StringComparison.OrdinalIgnoreCase))
chipRow.Children.Add(CreateAgentMetaChip(
"검토 권장",
"\uE946",
BrushFromHex("#A16207"),
BrushFromHex("#FFFBEB"),
BrushFromHex("#FDE68A")));
}
}
if (toolResultPresentation != null)
{
guidance = toolResultPresentation.FollowUpHint;
chipRow.Children.Add(CreateAgentMetaChip(
GetToolResultKindLabel(toolResultPresentation.Kind),
"\uE9CE",
BrushFromHex("#475569"),
BrushFromHex("#F8FAFC"),
BrushFromHex("#E2E8F0")));
if (toolResultPresentation.NeedsAttention)
chipRow.Children.Add(CreateAgentMetaChip(
"확인 필요",
"\uE814",
BrushFromHex("#B91C1C"),
BrushFromHex("#FEF2F2"),
BrushFromHex("#FECACA")));
if (string.Equals(toolResultPresentation.StatusKind, "approval_required", StringComparison.OrdinalIgnoreCase))
chipRow.Children.Add(CreateAgentMetaChip(
"승인 후 계속",
"\uE8D7",
BrushFromHex("#C2410C"),
BrushFromHex("#FFF7ED"),
BrushFromHex("#FED7AA")));
else if (string.Equals(toolResultPresentation.StatusKind, "partial", StringComparison.OrdinalIgnoreCase))
chipRow.Children.Add(CreateAgentMetaChip(
"후속 점검",
"\uE7BA",
BrushFromHex("#A16207"),
BrushFromHex("#FFFBEB"),
BrushFromHex("#FDE68A")));
}
if (!string.IsNullOrWhiteSpace(guidance))
{
var clipped = guidance.Length > 92 ? guidance[..92] + "..." : guidance;
if (permissionPresentation != null)
{
var titleBrush = string.Equals(permissionPresentation.Severity, "high", StringComparison.OrdinalIgnoreCase)
? BrushFromHex("#B91C1C")
: string.Equals(permissionPresentation.Severity, "medium", StringComparison.OrdinalIgnoreCase)
? BrushFromHex("#A16207")
: BrushFromHex("#2563EB");
var background = string.Equals(permissionPresentation.Severity, "high", StringComparison.OrdinalIgnoreCase)
? BrushFromHex("#FFF7F7")
: string.Equals(permissionPresentation.Severity, "medium", StringComparison.OrdinalIgnoreCase)
? BrushFromHex("#FFFBEB")
: BrushFromHex("#EFF6FF");
var border = string.Equals(permissionPresentation.Severity, "high", StringComparison.OrdinalIgnoreCase)
? BrushFromHex("#FECACA")
: string.Equals(permissionPresentation.Severity, "medium", StringComparison.OrdinalIgnoreCase)
? BrushFromHex("#FDE68A")
: BrushFromHex("#BFDBFE");
stack.Children.Add(CreateAgentMetaCallout("확인 포인트", clipped, titleBrush, background, border));
}
else if (toolResultPresentation != null)
{
var titleBrush = string.Equals(toolResultPresentation.StatusKind, "error", StringComparison.OrdinalIgnoreCase)
|| string.Equals(toolResultPresentation.StatusKind, "reject", StringComparison.OrdinalIgnoreCase)
? BrushFromHex("#B91C1C")
: string.Equals(toolResultPresentation.StatusKind, "approval_required", StringComparison.OrdinalIgnoreCase)
? BrushFromHex("#C2410C")
: BrushFromHex("#A16207");
var background = string.Equals(toolResultPresentation.StatusKind, "error", StringComparison.OrdinalIgnoreCase)
|| string.Equals(toolResultPresentation.StatusKind, "reject", StringComparison.OrdinalIgnoreCase)
? BrushFromHex("#FFF7F7")
: string.Equals(toolResultPresentation.StatusKind, "approval_required", StringComparison.OrdinalIgnoreCase)
? BrushFromHex("#FFF7ED")
: BrushFromHex("#FFFBEB");
var border = string.Equals(toolResultPresentation.StatusKind, "error", StringComparison.OrdinalIgnoreCase)
|| string.Equals(toolResultPresentation.StatusKind, "reject", StringComparison.OrdinalIgnoreCase)
? BrushFromHex("#FECACA")
: string.Equals(toolResultPresentation.StatusKind, "approval_required", StringComparison.OrdinalIgnoreCase)
? BrushFromHex("#FED7AA")
: BrushFromHex("#FDE68A");
stack.Children.Add(CreateAgentMetaCallout("다음 권장 작업", clipped, titleBrush, background, border));
}
}
if (chipRow.Children.Count > 0)
stack.Children.Add(chipRow);
}
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);
if (string.IsNullOrWhiteSpace(eventSummaryText))
{
eventSummaryText = evt.Type switch
{
AgentEventType.PermissionRequest or AgentEventType.PermissionGranted => permissionPresentation?.Description ?? "",
AgentEventType.ToolResult => toolResultPresentation?.Description ?? "",
_ => ""
};
}
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),
});
}
if (permissionPresentation != null || toolResultPresentation != null)
{
AppendAgentEventPresentationMeta(
sp,
evt,
permissionPresentation,
toolResultPresentation,
secondaryText,
hintBg,
borderColor);
}
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");
}
}