Files
AX-Copilot-Codex/src/AxCopilot/Views/ChatWindow.V2MessagePresentation.cs
lacvet 8cb08576d5 AX Agent 도구·스킬 정합성 재구성 및 실행 품질 보강
변경 목적:
- AX Agent의 도구 이름, 내부 설정, 스킬 정책, 실행 루프 사이의 불일치를 줄이고 전체 동작 품질을 높인다.
- claw-code 수준의 일관된 동작 품질을 참고하되 AX 구조에 맞는 고유한 카탈로그·정규화 레이어로 재구성한다.

핵심 수정사항:
- 도구 canonical id, legacy alias, 탭 노출, 설정 카테고리, read-only 분류를 중앙 카탈로그로 통합했다.
- ToolRegistry, AgentLoopService, 병렬 실행 분류, 권한 처리, 훅 처리, 스킬 allowed-tools 해석이 같은 이름 체계를 사용하도록 정리했다.
- Agent 설정/일반 설정/도움말의 도구 카드와 훅 편집기, 스킬 설명을 현재 런타임 구조에 맞게 갱신했다.
- 컨텍스트 압축, intent gate, spawn agents, session learning, model prompt adapter, workspace context 관련 변경과 테스트 추가를 함께 반영했다.
- 문서 이력과 비교/로드맵 문서를 최신 상태로 갱신했다.

검증 결과:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_toolcat\ -p:IntermediateOutputPath=obj\verify_toolcat\ : 경고 0 / 오류 0
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentToolCatalogTests -p:OutputPath=bin\verify_toolcat_tests\ -p:IntermediateOutputPath=obj\verify_toolcat_tests\ : 통과 8
2026-04-14 17:52:46 +09:00

265 lines
9.8 KiB
C#

using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using AxCopilot.Models;
using AxCopilot.Services;
namespace AxCopilot.Views;
public partial class ChatWindow
{
private UIElement CreateV2MessageElement(ChatMessage message)
{
var isUser = message.Role == "user";
return isUser ? CreateV2UserBubble(message) : CreateV2AssistantBlock(message);
}
private UIElement CreateV2UserBubble(ChatMessage message)
{
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
var hintBg = TryFindResource("HintBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
var msgMaxWidth = GetMessageMaxWidth();
var wrapper = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Right,
MaxWidth = msgMaxWidth * 0.85,
Margin = new Thickness(60, 12, 12, 4),
};
// 사용자 아이콘 + 이름 헤더
var header = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Thickness(2, 0, 0, 4),
};
header.Children.Add(new TextBlock
{
Text = "\uE77B", // 사용자 아이콘
FontFamily = s_segoeIconFont,
FontSize = 12,
Foreground = secondaryText,
VerticalAlignment = VerticalAlignment.Center,
});
header.Children.Add(new TextBlock
{
Text = "You",
FontSize = 11,
FontWeight = FontWeights.SemiBold,
Foreground = secondaryText,
Margin = new Thickness(6, 0, 0, 0),
VerticalAlignment = VerticalAlignment.Center,
});
var timestamp = message.Timestamp;
header.Children.Add(new TextBlock
{
Text = timestamp.ToString("HH:mm"),
FontSize = 10,
Foreground = secondaryText,
Opacity = 0.52,
Margin = new Thickness(8, 0, 0, 1),
VerticalAlignment = VerticalAlignment.Center,
});
wrapper.Children.Add(header);
// 메시지 본문
var bubble = new Border
{
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(12),
Padding = new Thickness(14, 10, 14, 10),
HorizontalAlignment = HorizontalAlignment.Stretch,
};
bubble.SetResourceReference(Border.BackgroundProperty, "HintBackground");
var content = message.Content ?? "";
if (string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase) ||
string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase))
{
MarkdownRenderer.EnableFilePathHighlight =
(System.Windows.Application.Current as App)?.SettingsService?.Settings.Llm.EnableFilePathHighlight ?? true;
MarkdownRenderer.EnableCodeSymbolHighlight = true;
bubble.Child = MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, hintBg);
}
else
{
MarkdownRenderer.EnableCodeSymbolHighlight = false;
bubble.Child = new TextBlock
{
Text = content,
TextAlignment = TextAlignment.Left,
FontSize = 12,
Foreground = primaryText,
TextWrapping = TextWrapping.Wrap,
LineHeight = 18,
};
}
wrapper.Children.Add(bubble);
// 우클릭 메뉴
var capturedContent = content;
wrapper.MouseRightButtonUp += (_, re) =>
{
re.Handled = true;
ShowMessageContextMenu(capturedContent, "user");
};
return wrapper;
}
private UIElement CreateV2AssistantBlock(ChatMessage message)
{
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
var hintBg = TryFindResource("HintBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
// Compaction 메타 메시지 처리
if (IsCompactionMetaMessage(message))
{
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
return CreateCompactionMetaCard(message, primaryText, secondaryText, hintBg, borderBrush, accentBrush);
}
var msgMaxWidth = GetMessageMaxWidth();
var container = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
Width = msgMaxWidth,
MaxWidth = msgMaxWidth,
Margin = new Thickness(0, 8, 0, 8),
};
// 에이전트 아이콘 + 이름 헤더
var (agentName, _, _) = GetAgentIdentity();
var (iconHost, iconPixels, iconGlows, iconRotate, iconScale) = CreateMiniLauncherIconEx(4.0, "none");
var header = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(2, 0, 0, 4),
};
header.Children.Add(iconHost);
header.Children.Add(new TextBlock
{
Text = agentName,
FontSize = 11,
FontWeight = FontWeights.SemiBold,
Foreground = secondaryText,
Margin = new Thickness(6, 0, 0, 0),
VerticalAlignment = VerticalAlignment.Center,
});
// 아이콘 애니메이션
var canvas = iconHost.Children.OfType<Canvas>().FirstOrDefault();
if (canvas != null)
{
var animState = new ChatIconAnimState
{
Host = iconHost, Canvas = canvas, Pixels = iconPixels,
Glows = iconGlows, Rotate = iconRotate, Scale = iconScale,
IsRandomMode = _settings.Settings.Launcher.EnableChatIconRandomAnimation,
};
StartChatIconAnimation(animState);
}
container.Children.Add(header);
// 마크다운 렌더 본문
var content = message.Content ?? "";
var codeBgBrush = TryFindResource("HintBackground") as Brush ?? Brushes.DarkGray;
MarkdownRenderer.EnableFilePathHighlight =
(System.Windows.Application.Current as App)?.SettingsService?.Settings.Llm.EnableFilePathHighlight ?? true;
MarkdownRenderer.EnableCodeSymbolHighlight =
string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase) ||
string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase);
var contentPanel = new Border
{
Background = Brushes.Transparent,
Padding = new Thickness(6, 4, 6, 4),
};
if (IsBranchContextMessage(content))
{
contentPanel.Child = MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, codeBgBrush);
}
else
{
contentPanel.Child = MarkdownRenderer.Render(content, primaryText, secondaryText, accentBrush, codeBgBrush);
}
container.Children.Add(contentPanel);
// 파일 퀵 액션
var outputFilePath = ExtractOutputFilePathFromContent(content);
if (!string.IsNullOrEmpty(outputFilePath) && System.IO.File.Exists(outputFilePath))
{
var quickActions = BuildFileQuickActions(outputFilePath);
quickActions.Margin = new Thickness(2, 4, 0, 2);
container.Children.Add(quickActions);
}
// 액션 바 (복사, 재생성 등)
var actionBar = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Left,
Margin = new Thickness(2, 2, 0, 0),
Opacity = 0,
};
var btnColor = secondaryText;
var capturedContent = content;
actionBar.Children.Add(CreateActionButton("\uE8C8", "복사", btnColor, () =>
{
try { Clipboard.SetText(capturedContent); } catch { }
}));
actionBar.Children.Add(CreateActionButton("\uE72C", "다시 생성", btnColor, () => _ = RegenerateLastAsync()));
var aiTimestamp = message.Timestamp;
actionBar.Children.Add(new TextBlock
{
Text = aiTimestamp.ToString("HH:mm"),
FontSize = 10,
Opacity = 0.6,
Foreground = btnColor,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(4, 0, 0, 1),
});
container.Children.Add(actionBar);
// 메타 정보 (토큰 등)
var assistantMeta = CreateAssistantMessageMetaText(message);
if (assistantMeta != null)
container.Children.Add(assistantMeta);
container.MouseEnter += (_, _) =>
actionBar.BeginAnimation(OpacityProperty,
new System.Windows.Media.Animation.DoubleAnimation(0.8, TimeSpan.FromMilliseconds(150)));
container.MouseLeave += (_, _) =>
actionBar.BeginAnimation(OpacityProperty,
new System.Windows.Media.Animation.DoubleAnimation(0, TimeSpan.FromMilliseconds(200)));
var aiContent = content;
container.MouseRightButtonUp += (_, re) =>
{
re.Handled = true;
ShowMessageContextMenu(aiContent, "assistant");
};
return container;
}
}