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:
2026-04-15 18:33:06 +09:00
parent 232d5457d5
commit 53838a046b
18 changed files with 550 additions and 425 deletions

View File

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