AX Agent ?? ?? ?? ??? ??? ?? ??? ????
- Code ??? ?? ???? ?? ??? ??? ? ?? ?? ???? ???? no-progress ??? ??? - ??? ?? ??? 1~2? ????? ????? ToolCall/ToolResult ?? ??? ?? ????? ????? ??? - ??? Thinking/LLM ?? ??? ??? ???? ??? ?? ?? ??? ??? ??? ????? ???? - Cowork/Code ??? ??? ?? ??? ???? ??? ??? ??? ?? ???? ? - README.md, docs/DEVELOPMENT.md ??? 2026-04-15 18:30 (KST) ???? ??? ?? - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_agent_ui_logs\\ -p:IntermediateOutputPath=obj\\verify_agent_ui_logs\\ - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopCodeQualityTests|AgentStatusNarrativeCatalogTests|AgentProgressSummarySanitizerTests" -p:OutputPath=bin\\verify_agent_ui_logs_tests\\ -p:IntermediateOutputPath=obj\\verify_agent_ui_logs_tests\\
This commit is contained in:
@@ -200,8 +200,220 @@ public static class MarkdownRenderer
|
||||
return panel;
|
||||
}
|
||||
|
||||
public static FrameworkElement RenderSelectable(string markdown, Brush textColor, Brush secondaryColor, Brush accentColor, Brush codeBg)
|
||||
{
|
||||
var document = BuildSelectableDocument(markdown, textColor, secondaryColor, accentColor, codeBg);
|
||||
var viewer = new RichTextBox
|
||||
{
|
||||
Document = document,
|
||||
IsReadOnly = true,
|
||||
IsDocumentEnabled = true,
|
||||
Background = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Disabled,
|
||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
|
||||
AcceptsReturn = true,
|
||||
};
|
||||
|
||||
void UpdatePageWidth()
|
||||
{
|
||||
var width = System.Math.Max(0, viewer.ActualWidth - 8);
|
||||
viewer.Document.PageWidth = width <= 0 ? 640 : width;
|
||||
}
|
||||
|
||||
viewer.Loaded += (_, _) => UpdatePageWidth();
|
||||
viewer.SizeChanged += (_, _) => UpdatePageWidth();
|
||||
return viewer;
|
||||
}
|
||||
|
||||
private static FlowDocument BuildSelectableDocument(string markdown, Brush textColor, Brush secondaryColor, Brush accentColor, Brush codeBg)
|
||||
{
|
||||
var document = new FlowDocument
|
||||
{
|
||||
PagePadding = new Thickness(0),
|
||||
Background = Brushes.Transparent,
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(markdown))
|
||||
return document;
|
||||
|
||||
var lines = markdown.Replace("\r\n", "\n").Split('\n');
|
||||
var i = 0;
|
||||
|
||||
while (i < lines.Length)
|
||||
{
|
||||
var line = lines[i];
|
||||
|
||||
if (line.TrimStart().StartsWith("```"))
|
||||
{
|
||||
var lang = line.TrimStart().Length > 3 ? line.TrimStart()[3..].Trim() : "";
|
||||
var codeLines = new System.Text.StringBuilder();
|
||||
i++;
|
||||
while (i < lines.Length && !lines[i].TrimStart().StartsWith("```"))
|
||||
{
|
||||
codeLines.AppendLine(lines[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i < lines.Length)
|
||||
i++;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(lang))
|
||||
{
|
||||
document.Blocks.Add(new Paragraph(new Run(lang))
|
||||
{
|
||||
FontSize = 10.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = accentColor,
|
||||
Margin = new Thickness(0, 6, 0, 2),
|
||||
});
|
||||
}
|
||||
|
||||
var codeParagraph = new Paragraph
|
||||
{
|
||||
Margin = new Thickness(0, 0, 0, 8),
|
||||
Padding = new Thickness(12, 10, 12, 10),
|
||||
Background = codeBg,
|
||||
BorderBrush = new SolidColorBrush(Color.FromArgb(0x30, 0xFF, 0xFF, 0xFF)),
|
||||
BorderThickness = new Thickness(1),
|
||||
FontFamily = new FontFamily("Cascadia Code, Consolas, monospace"),
|
||||
FontSize = 12.5,
|
||||
};
|
||||
AppendSyntaxHighlightedInlines(codeParagraph.Inlines, codeLines.ToString().TrimEnd(), lang, textColor);
|
||||
document.Blocks.Add(codeParagraph);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
document.Blocks.Add(new Paragraph(new Run(" "))
|
||||
{
|
||||
Margin = new Thickness(0, 2, 0, 2),
|
||||
FontSize = 4,
|
||||
});
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Regex.IsMatch(line.Trim(), @"^-{3,}$|^\*{3,}$"))
|
||||
{
|
||||
document.Blocks.Add(new BlockUIContainer(new Border
|
||||
{
|
||||
Height = 1,
|
||||
Background = secondaryColor,
|
||||
Opacity = 0.3,
|
||||
Margin = new Thickness(0, 8, 0, 8),
|
||||
}));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith('#'))
|
||||
{
|
||||
var level = 0;
|
||||
while (level < line.Length && line[level] == '#') level++;
|
||||
var headerText = line[level..].Trim();
|
||||
var fontSize = level switch { 1 => 20.0, 2 => 17.0, 3 => 15.0, _ => 14.0 };
|
||||
var paragraph = new Paragraph
|
||||
{
|
||||
FontSize = fontSize,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Foreground = textColor,
|
||||
Margin = new Thickness(0, level == 1 ? 12 : 8, 0, 4),
|
||||
};
|
||||
AddInlines(paragraph.Inlines, headerText, textColor, accentColor, codeBg, selectable: true);
|
||||
document.Blocks.Add(paragraph);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Regex.IsMatch(line, @"^\s*[-*]\s") || Regex.IsMatch(line, @"^\s*\d+\.\s"))
|
||||
{
|
||||
var match = Regex.Match(line, @"^(\s*)([-*]|\d+\.)\s(.*)");
|
||||
if (match.Success)
|
||||
{
|
||||
var indent = match.Groups[1].Value.Length / 2;
|
||||
var bullet = match.Groups[2].Value;
|
||||
var content = match.Groups[3].Value;
|
||||
var paragraph = new Paragraph
|
||||
{
|
||||
Margin = new Thickness(12 + indent * 16, 2, 0, 2),
|
||||
Foreground = textColor,
|
||||
FontSize = 13.5,
|
||||
};
|
||||
paragraph.Inlines.Add(new Run(bullet is "-" or "*" ? "• " : $"{bullet} ")
|
||||
{
|
||||
Foreground = accentColor,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
});
|
||||
AddInlines(paragraph.Inlines, content, textColor, accentColor, codeBg, selectable: true);
|
||||
document.Blocks.Add(paragraph);
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.TrimStart().StartsWith('>'))
|
||||
{
|
||||
var quoteLines = new List<string>();
|
||||
while (i < lines.Length && lines[i].TrimStart().StartsWith('>'))
|
||||
{
|
||||
var ql = lines[i].TrimStart();
|
||||
quoteLines.Add(ql.Length > 1 ? ql[1..].TrimStart() : "");
|
||||
i++;
|
||||
}
|
||||
|
||||
var quoteParagraph = new Paragraph
|
||||
{
|
||||
Margin = new Thickness(4, 4, 0, 4),
|
||||
Padding = new Thickness(12, 6, 8, 6),
|
||||
BorderBrush = accentColor,
|
||||
BorderThickness = new Thickness(3, 0, 0, 0),
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF)),
|
||||
FontStyle = FontStyles.Italic,
|
||||
FontSize = 13,
|
||||
Foreground = secondaryColor,
|
||||
LineHeight = 20,
|
||||
};
|
||||
AddInlines(quoteParagraph.Inlines, string.Join("\n", quoteLines), textColor, accentColor, codeBg, selectable: true);
|
||||
document.Blocks.Add(quoteParagraph);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.Contains('|') && line.Trim().StartsWith('|'))
|
||||
{
|
||||
var tableRows = new List<string>();
|
||||
while (i < lines.Length && lines[i].Contains('|'))
|
||||
{
|
||||
tableRows.Add(lines[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
var table = CreateMarkdownTable(tableRows, textColor, accentColor, codeBg);
|
||||
if (table != null)
|
||||
document.Blocks.Add(new BlockUIContainer(table));
|
||||
continue;
|
||||
}
|
||||
|
||||
var paragraphBlock = new Paragraph
|
||||
{
|
||||
FontSize = 13.5,
|
||||
Foreground = textColor,
|
||||
LineHeight = 22,
|
||||
Margin = new Thickness(0, 2, 0, 2),
|
||||
};
|
||||
AddInlines(paragraphBlock.Inlines, line, textColor, accentColor, codeBg, selectable: true);
|
||||
document.Blocks.Add(paragraphBlock);
|
||||
i++;
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
/// <summary>인라인 마크다운 처리: **볼드**, *이탤릭*, `코드`, ~~취소선~~, [링크](url)</summary>
|
||||
private static void AddInlines(InlineCollection inlines, string text, Brush textColor, Brush accentColor, Brush codeBg)
|
||||
private static void AddInlines(InlineCollection inlines, string text, Brush textColor, Brush accentColor, Brush codeBg, bool selectable = false)
|
||||
{
|
||||
// 패턴: [link](url) | ~~strikethrough~~ | **bold** | *italic* | `code` | 일반텍스트
|
||||
var pattern = @"(\[([^\]]+)\]\(([^)]+)\)|~~(.+?)~~|\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`|([^*`~\[]+))";
|
||||
@@ -242,21 +454,34 @@ public static class MarkdownRenderer
|
||||
}
|
||||
else if (m.Groups[7].Success) // `code`
|
||||
{
|
||||
var codeBorder = new Border
|
||||
if (selectable)
|
||||
{
|
||||
Background = codeBg,
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Padding = new Thickness(5, 1, 5, 1),
|
||||
Margin = new Thickness(1, 0, 1, 0),
|
||||
Child = new TextBlock
|
||||
inlines.Add(new Run(m.Groups[7].Value)
|
||||
{
|
||||
Text = m.Groups[7].Value,
|
||||
FontFamily = new FontFamily("Cascadia Code, Consolas, monospace"),
|
||||
FontSize = 12.5,
|
||||
Foreground = accentColor
|
||||
}
|
||||
};
|
||||
inlines.Add(new InlineUIContainer(codeBorder) { BaselineAlignment = BaselineAlignment.Center });
|
||||
Foreground = accentColor,
|
||||
Background = codeBg,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var codeBorder = new Border
|
||||
{
|
||||
Background = codeBg,
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Padding = new Thickness(5, 1, 5, 1),
|
||||
Margin = new Thickness(1, 0, 1, 0),
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = m.Groups[7].Value,
|
||||
FontFamily = new FontFamily("Cascadia Code, Consolas, monospace"),
|
||||
FontSize = 12.5,
|
||||
Foreground = accentColor
|
||||
}
|
||||
};
|
||||
inlines.Add(new InlineUIContainer(codeBorder) { BaselineAlignment = BaselineAlignment.Center });
|
||||
}
|
||||
}
|
||||
else if (m.Groups[8].Success) // 일반 텍스트
|
||||
{
|
||||
@@ -797,105 +1022,59 @@ public static class MarkdownRenderer
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static void ApplySyntaxHighlighting(TextBlock tb, string code, string lang, Brush defaultColor)
|
||||
{
|
||||
tb.Inlines.Clear();
|
||||
AppendSyntaxHighlightedInlines(tb.Inlines, code, lang, defaultColor);
|
||||
}
|
||||
|
||||
private static void AppendSyntaxHighlightedInlines(InlineCollection inlines, string code, string lang, Brush defaultColor)
|
||||
{
|
||||
if (string.IsNullOrEmpty(lang) || string.IsNullOrEmpty(code))
|
||||
{
|
||||
tb.Text = code;
|
||||
inlines.Add(new Run(code) { Foreground = defaultColor });
|
||||
return;
|
||||
}
|
||||
|
||||
var keywords = GetKeywordsForLang(lang);
|
||||
var isCommentHash = lang.ToLowerInvariant() is "python" or "py" or "bash" or "sh" or "shell"
|
||||
or "ruby" or "rb" or "yaml" or "yml" or "toml" or "powershell" or "ps1";
|
||||
|
||||
foreach (Match m in SyntaxPattern.Matches(code))
|
||||
{
|
||||
if (m.Groups[1].Success) // comment
|
||||
{
|
||||
var commentText = m.Groups[1].Value;
|
||||
// # 주석은 해당 언어에서만 적용
|
||||
if (commentText.StartsWith('#') && !isCommentHash)
|
||||
{
|
||||
// # 이 주석이 아닌 언어: 일반 텍스트로 처리
|
||||
tb.Inlines.Add(new Run(commentText) { Foreground = defaultColor });
|
||||
}
|
||||
else
|
||||
{
|
||||
tb.Inlines.Add(new Run(commentText) { Foreground = CommentBrush, FontStyle = FontStyles.Italic });
|
||||
}
|
||||
}
|
||||
else if (m.Groups[2].Success) // string
|
||||
{
|
||||
tb.Inlines.Add(new Run(m.Groups[2].Value) { Foreground = StringBrush });
|
||||
}
|
||||
else if (m.Groups[3].Success) // number
|
||||
{
|
||||
tb.Inlines.Add(new Run(m.Groups[3].Value) { Foreground = NumberBrush });
|
||||
}
|
||||
else if (m.Groups[4].Success) // type
|
||||
{
|
||||
var word = m.Groups[4].Value;
|
||||
tb.Inlines.Add(new Run(word) { Foreground = keywords.Contains(word) ? KeywordBrush : TypeBrush });
|
||||
}
|
||||
else if (m.Groups[5].Success) // method
|
||||
{
|
||||
var word = m.Groups[5].Value;
|
||||
if (keywords.Contains(word))
|
||||
tb.Inlines.Add(new Run(word) { Foreground = KeywordBrush });
|
||||
else
|
||||
tb.Inlines.Add(new Run(word) { Foreground = MethodBrush });
|
||||
}
|
||||
else if (m.Groups[6].Success) // identifier
|
||||
{
|
||||
var word = m.Groups[6].Value;
|
||||
if (keywords.Contains(word))
|
||||
tb.Inlines.Add(new Run(word) { Foreground = KeywordBrush });
|
||||
else
|
||||
tb.Inlines.Add(new Run(word) { Foreground = defaultColor });
|
||||
}
|
||||
}
|
||||
|
||||
// Regex가 매치하지 못한 나머지 문자(공백, 기호 등)를 채움
|
||||
// Inlines로 빌드했으므로 원본과 비교하여 누락된 부분 보완
|
||||
// 대신 각 매치 사이의 간격을 처리하기 위해 인덱스 기반 접근
|
||||
tb.Inlines.Clear();
|
||||
int lastEnd = 0;
|
||||
foreach (Match m in SyntaxPattern.Matches(code))
|
||||
{
|
||||
if (m.Index > lastEnd)
|
||||
tb.Inlines.Add(new Run(code[lastEnd..m.Index]) { Foreground = defaultColor });
|
||||
inlines.Add(new Run(code[lastEnd..m.Index]) { Foreground = defaultColor });
|
||||
|
||||
if (m.Groups[1].Success)
|
||||
{
|
||||
var commentText = m.Groups[1].Value;
|
||||
if (commentText.StartsWith('#') && !isCommentHash)
|
||||
tb.Inlines.Add(new Run(commentText) { Foreground = defaultColor });
|
||||
inlines.Add(new Run(commentText) { Foreground = defaultColor });
|
||||
else
|
||||
tb.Inlines.Add(new Run(commentText) { Foreground = CommentBrush, FontStyle = FontStyles.Italic });
|
||||
inlines.Add(new Run(commentText) { Foreground = CommentBrush, FontStyle = FontStyles.Italic });
|
||||
}
|
||||
else if (m.Groups[2].Success)
|
||||
tb.Inlines.Add(new Run(m.Groups[2].Value) { Foreground = StringBrush });
|
||||
inlines.Add(new Run(m.Groups[2].Value) { Foreground = StringBrush });
|
||||
else if (m.Groups[3].Success)
|
||||
tb.Inlines.Add(new Run(m.Groups[3].Value) { Foreground = NumberBrush });
|
||||
inlines.Add(new Run(m.Groups[3].Value) { Foreground = NumberBrush });
|
||||
else if (m.Groups[4].Success)
|
||||
{
|
||||
var word = m.Groups[4].Value;
|
||||
tb.Inlines.Add(new Run(word) { Foreground = keywords.Contains(word) ? KeywordBrush : TypeBrush });
|
||||
inlines.Add(new Run(word) { Foreground = keywords.Contains(word) ? KeywordBrush : TypeBrush });
|
||||
}
|
||||
else if (m.Groups[5].Success)
|
||||
{
|
||||
var word = m.Groups[5].Value;
|
||||
tb.Inlines.Add(new Run(word) { Foreground = keywords.Contains(word) ? KeywordBrush : MethodBrush });
|
||||
inlines.Add(new Run(word) { Foreground = keywords.Contains(word) ? KeywordBrush : MethodBrush });
|
||||
}
|
||||
else if (m.Groups[6].Success)
|
||||
{
|
||||
var word = m.Groups[6].Value;
|
||||
tb.Inlines.Add(new Run(word) { Foreground = keywords.Contains(word) ? KeywordBrush : defaultColor });
|
||||
inlines.Add(new Run(word) { Foreground = keywords.Contains(word) ? KeywordBrush : defaultColor });
|
||||
}
|
||||
|
||||
lastEnd = m.Index + m.Length;
|
||||
}
|
||||
if (lastEnd < code.Length)
|
||||
tb.Inlines.Add(new Run(code[lastEnd..]) { Foreground = defaultColor });
|
||||
inlines.Add(new Run(code[lastEnd..]) { Foreground = defaultColor });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user