Files
AX-Copilot-Codex/src/AxCopilot/Views/ChatWindow.AgentEventRendering.cs
lacvet d5c1266d3e
Some checks failed
Release Gate / gate (push) Has been cancelled
상태선/권한 카탈로그 구조 정리와 계획 모드 표현 잔재 제거
- OperationalStatusPresentationCatalog를 추가해 compact strip과 quick strip의 색상/노출 계산을 AppStateService 밖으로 분리함

- PermissionRequestPresentationCatalog와 ToolResultPresentationCatalog에 Kind/Description 메타를 추가해 transcript fallback 설명을 타입 기반으로 정리함

- PermissionModePresentationCatalog와 ChatWindow.PermissionPresentation에서 제거된 계획 모드 표현 분기를 걷어 권한 UI를 실제 지원 모드만 다루도록 단순화함

- README, DEVELOPMENT, claw-code parity plan 문서를 2026-04-06 09:36 (KST) 기준으로 갱신함

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-06 09:44:53 +09:00

431 lines
19 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 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),
});
}
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");
}
}