using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using AxCopilot.Services;
using AxCopilot.Services.Agent;
namespace AxCopilot.Views;
public partial class ChatWindow
{
/// V2: 단일 에이전트 이벤트를 렌더링 (ToolCall/ToolResult 병합되지 않은 경우)
private UIElement CreateV2AgentEventElement(AgentEvent agentEvent)
{
return agentEvent.Type switch
{
AgentEventType.Thinking => CreateV2ThinkingBlock(agentEvent),
AgentEventType.ToolCall => CreateV2ToolCallOnlyCard(agentEvent),
AgentEventType.ToolResult => CreateV2ToolResultOnlyCard(agentEvent),
AgentEventType.Complete => CreateV2CompleteBanner(agentEvent),
AgentEventType.Error => CreateV2ErrorBanner(agentEvent),
_ => CreateV2GenericEventPill(agentEvent),
};
}
/// V2: ToolCall + ToolResult 쌍을 병합한 실행 카드
private UIElement CreateV2ToolExecutionCard(AgentEvent toolCall, AgentEvent toolResult)
{
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 hintBg = TryFindResource("HintBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
var msgMaxWidth = GetMessageMaxWidth();
var (icon, iconColor) = GetV2ToolIcon(toolCall.ToolName);
var elapsed = NormalizeProgressElapsedMs(toolResult.ElapsedMs);
var elapsedText = elapsed > 0 ? $"{elapsed / 1000.0:F1}s" : "";
var isSuccess = toolResult.Success;
// 외부 컨테이너 — 왼쪽 세로선 + 카드
var outerGrid = new Grid
{
HorizontalAlignment = HorizontalAlignment.Center,
Width = msgMaxWidth,
MaxWidth = msgMaxWidth,
Margin = new Thickness(0, 2, 0, 2),
};
outerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
outerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
// 왼쪽 세로선
var lineColor = isSuccess
? new SolidColorBrush(Color.FromArgb(0x60, 0x66, 0xBB, 0x6A))
: new SolidColorBrush(Color.FromArgb(0x60, 0xEF, 0x53, 0x50));
var verticalLine = new Border
{
Width = 2,
Background = lineColor,
CornerRadius = new CornerRadius(1),
Margin = new Thickness(12, 0, 8, 0),
VerticalAlignment = VerticalAlignment.Stretch,
};
Grid.SetColumn(verticalLine, 0);
outerGrid.Children.Add(verticalLine);
// 메인 카드
var card = new Border
{
Background = hintBg,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(10),
Padding = new Thickness(12, 8, 12, 8),
Cursor = Cursors.Hand,
};
Grid.SetColumn(card, 1);
var cardStack = new StackPanel();
// 헤더: 아이콘 + 도구명 + 파일경로 + 소요시간
var headerGrid = new Grid();
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // 아이콘
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // 도구명
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); // 파일경로
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // 상태+시간
var statusIcon = isSuccess ? "\uE73E" : "\uE711"; // 체크 or X
var statusColor = isSuccess
? new SolidColorBrush(Color.FromRgb(0x66, 0xBB, 0x6A))
: new SolidColorBrush(Color.FromRgb(0xEF, 0x53, 0x50));
var iconTb = new TextBlock
{
Text = icon,
FontSize = 13,
Foreground = iconColor,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 6, 0),
};
Grid.SetColumn(iconTb, 0);
headerGrid.Children.Add(iconTb);
var toolNameTb = new TextBlock
{
Text = GetV2ToolDisplayName(toolCall.ToolName),
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
};
Grid.SetColumn(toolNameTb, 1);
headerGrid.Children.Add(toolNameTb);
// 파일 경로 (있으면)
var filePath = toolCall.FilePath ?? toolResult.FilePath;
if (!string.IsNullOrWhiteSpace(filePath))
{
var pathTb = new TextBlock
{
Text = TruncateFilePath(filePath, 60),
FontSize = 10.5,
Foreground = secondaryText,
Opacity = 0.8,
VerticalAlignment = VerticalAlignment.Center,
TextTrimming = TextTrimming.CharacterEllipsis,
ToolTip = filePath,
};
Grid.SetColumn(pathTb, 2);
headerGrid.Children.Add(pathTb);
}
// 상태 아이콘 + 소요시간
var statusPanel = new StackPanel { Orientation = Orientation.Horizontal };
statusPanel.Children.Add(new TextBlock
{
Text = statusIcon,
FontFamily = s_segoeIconFont,
FontSize = 11,
Foreground = statusColor,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 4, 0),
});
if (!string.IsNullOrEmpty(elapsedText))
{
statusPanel.Children.Add(new TextBlock
{
Text = elapsedText,
FontSize = 10,
Foreground = secondaryText,
Opacity = 0.7,
VerticalAlignment = VerticalAlignment.Center,
});
}
Grid.SetColumn(statusPanel, 3);
headerGrid.Children.Add(statusPanel);
cardStack.Children.Add(headerGrid);
// Summary 텍스트 (있으면)
var summary = toolCall.Summary ?? toolResult.Summary;
if (!string.IsNullOrWhiteSpace(summary))
{
var summaryTb = new TextBlock
{
Text = summary,
FontSize = 10.5,
Foreground = secondaryText,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 4, 0, 0),
MaxHeight = 40,
TextTrimming = TextTrimming.CharacterEllipsis,
};
cardStack.Children.Add(summaryTb);
}
// 접힘/펼침 상세 영역
var detailBorder = new Border
{
Visibility = Visibility.Collapsed,
Margin = new Thickness(0, 6, 0, 0),
};
var detailStack = new StackPanel();
var codeBg = TryFindResource("HintBackground") as Brush ?? Brushes.DarkGray;
// 도구 입력 파라미터 (ToolInput이 있으면 표시)
var toolInput = toolCall.ToolInput;
if (!string.IsNullOrWhiteSpace(toolInput))
{
detailStack.Children.Add(new TextBlock
{
Text = "입력",
FontSize = 10,
FontWeight = FontWeights.SemiBold,
Foreground = secondaryText,
Opacity = 0.7,
Margin = new Thickness(0, 0, 0, 3),
});
detailStack.Children.Add(MarkdownRenderer.Render(
$"```json\n{TruncateForDisplay(toolInput, 1200)}\n```",
primaryText, secondaryText, accentBrush, codeBg));
}
// 도구 결과 상세 내용
var resultSummary = toolResult.Summary;
if (!string.IsNullOrWhiteSpace(resultSummary))
{
// 입력이 이미 있으면 "결과" 라벨 추가
if (detailStack.Children.Count > 0)
{
detailStack.Children.Add(new TextBlock
{
Text = "결과",
FontSize = 10,
FontWeight = FontWeights.SemiBold,
Foreground = secondaryText,
Opacity = 0.7,
Margin = new Thickness(0, 6, 0, 3),
});
}
detailStack.Children.Add(MarkdownRenderer.Render(
$"```\n{TruncateForDisplay(resultSummary, 2000)}\n```",
primaryText, secondaryText, accentBrush, codeBg));
}
// 토큰 메타 정보
var inputTokens = toolResult.InputTokens;
var outputTokens = toolResult.OutputTokens;
if (inputTokens > 0 || outputTokens > 0)
{
var metaParts = new System.Collections.Generic.List();
if (inputTokens > 0) metaParts.Add($"입력: {inputTokens:N0} 토큰");
if (outputTokens > 0) metaParts.Add($"출력: {outputTokens:N0} 토큰");
detailStack.Children.Add(new TextBlock
{
Text = string.Join(" · ", metaParts),
FontSize = 9.5,
Foreground = secondaryText,
Opacity = 0.6,
Margin = new Thickness(0, 4, 0, 0),
});
}
detailBorder.Child = detailStack;
// 상세 내용이 없으면 화살표도 숨김
var hasDetail = detailStack.Children.Count > 0;
cardStack.Children.Add(detailBorder);
// 펼치기 토글 화살표 (상세 내용이 있을 때만 표시)
var arrowTb = new TextBlock
{
Text = "\uE76C", // 아래 화살표
FontFamily = s_segoeIconFont,
FontSize = 9,
Foreground = secondaryText,
Opacity = 0.7,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 3, 0, 0),
Cursor = Cursors.Hand,
Visibility = hasDetail ? Visibility.Visible : Visibility.Collapsed,
};
cardStack.Children.Add(arrowTb);
card.Child = cardStack;
outerGrid.Children.Add(card);
// 클릭 → 접힘/펼침 토글
card.MouseLeftButtonUp += (_, e) =>
{
e.Handled = true;
var isExpanded = detailBorder.Visibility == Visibility.Visible;
detailBorder.Visibility = isExpanded ? Visibility.Collapsed : Visibility.Visible;
arrowTb.Text = isExpanded ? "\uE76C" : "\uE76B"; // 아래↔위
};
// 호버 효과 (부드러운 전환)
card.MouseEnter += (_, _) =>
{
card.BeginAnimation(UIElement.OpacityProperty,
new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(120)));
card.Background = TryFindResource("ItemHoverBackground") as Brush ?? hintBg;
};
card.MouseLeave += (_, _) =>
{
card.BeginAnimation(UIElement.OpacityProperty,
new DoubleAnimation(0.92, TimeSpan.FromMilliseconds(200)));
card.Background = hintBg;
};
card.Opacity = 0.92;
return outerGrid;
}
/// V2: Thinking 블록 — 점선 테두리, 이탤릭 텍스트
private UIElement CreateV2ThinkingBlock(AgentEvent agentEvent)
{
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var msgMaxWidth = GetMessageMaxWidth();
var summary = agentEvent.Summary;
if (string.IsNullOrWhiteSpace(summary))
return new Border { Width = 0, Height = 0 }; // 빈 thinking은 숨김
// agent_wait / context_compaction 같은 운영 이벤트
if (string.Equals(agentEvent.ToolName, "agent_wait", StringComparison.OrdinalIgnoreCase)
|| string.Equals(agentEvent.ToolName, "context_compaction", StringComparison.OrdinalIgnoreCase))
{
return CreateV2GenericEventPill(agentEvent);
}
var outerGrid = new Grid
{
HorizontalAlignment = HorizontalAlignment.Center,
Width = msgMaxWidth,
MaxWidth = msgMaxWidth,
Margin = new Thickness(0, 2, 0, 2),
};
outerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
outerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
// 왼쪽 세로선 (사고 과정 = 파란색 점선 느낌)
var thinkLine = new Border
{
Width = 2,
Background = new SolidColorBrush(Color.FromArgb(0x60, 0x59, 0xA5, 0xF5)),
CornerRadius = new CornerRadius(1),
Margin = new Thickness(12, 0, 8, 0),
};
Grid.SetColumn(thinkLine, 0);
outerGrid.Children.Add(thinkLine);
var thinkCard = new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x1C, 0x59, 0xA5, 0xF5)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, 0x59, 0xA5, 0xF5)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(10, 6, 10, 6),
};
Grid.SetColumn(thinkCard, 1);
var thinkStack = new StackPanel { Orientation = Orientation.Horizontal };
thinkStack.Children.Add(new TextBlock
{
Text = "\uE915", // 전구 아이콘
FontFamily = s_segoeIconFont,
FontSize = 11,
Foreground = new SolidColorBrush(Color.FromRgb(0x59, 0xA5, 0xF5)),
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 1, 6, 0),
});
thinkStack.Children.Add(new TextBlock
{
Text = summary.Length > 200 ? summary[..200] + "..." : summary,
FontSize = 11,
FontStyle = FontStyles.Italic,
Foreground = secondaryText,
Opacity = 0.88,
TextWrapping = TextWrapping.Wrap,
MaxWidth = msgMaxWidth - 60,
});
thinkCard.Child = thinkStack;
outerGrid.Children.Add(thinkCard);
return outerGrid;
}
/// V2: ToolCall만 있고 ToolResult가 없는 경우
private UIElement CreateV2ToolCallOnlyCard(AgentEvent agentEvent)
{
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 hintBg = TryFindResource("HintBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
var msgMaxWidth = GetMessageMaxWidth();
var (icon, iconColor) = GetV2ToolIcon(agentEvent.ToolName);
var outerGrid = new Grid
{
HorizontalAlignment = HorizontalAlignment.Center,
Width = msgMaxWidth,
MaxWidth = msgMaxWidth,
Margin = new Thickness(0, 2, 0, 2),
};
outerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
outerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
var line = new Border
{
Width = 2,
Background = new SolidColorBrush(Color.FromArgb(0x40, 0x59, 0xA5, 0xF5)),
CornerRadius = new CornerRadius(1),
Margin = new Thickness(12, 0, 8, 0),
};
Grid.SetColumn(line, 0);
outerGrid.Children.Add(line);
var pill = new Border
{
Background = hintBg,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(10, 6, 10, 6),
};
Grid.SetColumn(pill, 1);
var sp = new StackPanel { Orientation = Orientation.Horizontal };
sp.Children.Add(new TextBlock
{
Text = icon,
FontSize = 12,
Foreground = iconColor,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 6, 0),
});
sp.Children.Add(new TextBlock
{
Text = GetV2ToolDisplayName(agentEvent.ToolName),
FontSize = 11,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
VerticalAlignment = VerticalAlignment.Center,
});
if (!string.IsNullOrWhiteSpace(agentEvent.FilePath))
{
sp.Children.Add(new TextBlock
{
Text = TruncateFilePath(agentEvent.FilePath, 50),
FontSize = 10,
Foreground = secondaryText,
Opacity = 0.7,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(8, 0, 0, 0),
});
}
pill.Child = sp;
outerGrid.Children.Add(pill);
return outerGrid;
}
/// V2: ToolResult만 있고 ToolCall이 없는 경우
private UIElement CreateV2ToolResultOnlyCard(AgentEvent agentEvent)
{
// ToolCall과 유사하지만 결과 표시
return CreateV2ToolCallOnlyCard(agentEvent);
}
/// V2: 작업 완료 배너
private UIElement CreateV2CompleteBanner(AgentEvent agentEvent)
{
var msgMaxWidth = GetMessageMaxWidth();
var elapsed = NormalizeProgressElapsedMs(agentEvent.ElapsedMs);
var elapsedText = elapsed > 0 ? $" · {elapsed / 1000.0:F1}s" : "";
var tokenInfo = "";
if (agentEvent.InputTokens > 0 || agentEvent.OutputTokens > 0)
{
var parts = new System.Collections.Generic.List();
if (agentEvent.InputTokens > 0) parts.Add($"입력 {agentEvent.InputTokens:N0}");
if (agentEvent.OutputTokens > 0) parts.Add($"출력 {agentEvent.OutputTokens:N0}");
tokenInfo = $" · {string.Join("/", parts)} 토큰";
}
var banner = new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x24, 0x66, 0xBB, 0x6A)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x50, 0x66, 0xBB, 0x6A)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(14, 8, 14, 8),
Margin = new Thickness(0, 6, 0, 4),
HorizontalAlignment = HorizontalAlignment.Center,
MaxWidth = msgMaxWidth,
};
var sp = new StackPanel { Orientation = Orientation.Horizontal };
sp.Children.Add(new TextBlock
{
Text = "\uE73E", // 체크마크
FontFamily = s_segoeIconFont,
FontSize = 12,
Foreground = new SolidColorBrush(Color.FromRgb(0x66, 0xBB, 0x6A)),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 6, 0),
});
sp.Children.Add(new TextBlock
{
Text = $"작업 완료{elapsedText}{tokenInfo}",
FontSize = 11,
Foreground = new SolidColorBrush(Color.FromRgb(0x66, 0xBB, 0x6A)),
VerticalAlignment = VerticalAlignment.Center,
});
banner.Child = sp;
return banner;
}
/// V2: 에러 배너
private UIElement CreateV2ErrorBanner(AgentEvent agentEvent)
{
var msgMaxWidth = GetMessageMaxWidth();
var banner = new Border
{
Background = new SolidColorBrush(Color.FromArgb(0x24, 0xEF, 0x53, 0x50)),
BorderBrush = new SolidColorBrush(Color.FromArgb(0x50, 0xEF, 0x53, 0x50)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(14, 8, 14, 8),
Margin = new Thickness(0, 6, 0, 4),
HorizontalAlignment = HorizontalAlignment.Center,
MaxWidth = msgMaxWidth,
};
var sp = new StackPanel { Orientation = Orientation.Horizontal };
sp.Children.Add(new TextBlock
{
Text = "\uE711", // X
FontFamily = s_segoeIconFont,
FontSize = 12,
Foreground = new SolidColorBrush(Color.FromRgb(0xEF, 0x53, 0x50)),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 6, 0),
});
sp.Children.Add(new TextBlock
{
Text = $"오류 발생: {agentEvent.Summary ?? "알 수 없는 오류"}",
FontSize = 11,
Foreground = new SolidColorBrush(Color.FromRgb(0xEF, 0x53, 0x50)),
TextWrapping = TextWrapping.Wrap,
VerticalAlignment = VerticalAlignment.Center,
});
banner.Child = sp;
return banner;
}
/// V2: 일반 이벤트 pill (분류 안 되는 이벤트)
private UIElement CreateV2GenericEventPill(AgentEvent agentEvent)
{
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var hintBg = TryFindResource("HintBackground") as Brush
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var msgMaxWidth = GetMessageMaxWidth();
var displayText = agentEvent.Summary ?? agentEvent.ToolName ?? agentEvent.Type.ToString();
var pill = new Border
{
Background = hintBg,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8),
Padding = new Thickness(10, 4, 10, 4),
Margin = new Thickness(0, 2, 0, 2),
HorizontalAlignment = HorizontalAlignment.Center,
MaxWidth = msgMaxWidth,
};
pill.Child = new TextBlock
{
Text = displayText,
FontSize = 10.5,
Foreground = secondaryText,
TextTrimming = TextTrimming.CharacterEllipsis,
};
return pill;
}
// ─── V2 헬퍼 ────────────────────────────────────────────────────────
private static (string Icon, Brush Color) GetV2ToolIcon(string? toolName)
{
if (string.IsNullOrWhiteSpace(toolName))
return ("\uE90F", Brushes.Gray); // 기어
var lower = toolName.ToLowerInvariant();
if (lower.Contains("read") || lower.Contains("document_read"))
return ("\uE8E5", new SolidColorBrush(Color.FromRgb(0x42, 0xA5, 0xF5))); // 파일 읽기 파랑
if (lower.Contains("write") || lower.Contains("edit") || lower.Contains("create"))
return ("\uE70F", new SolidColorBrush(Color.FromRgb(0x66, 0xBB, 0x6A))); // 편집 초록
if (lower.Contains("search") || lower.Contains("web"))
return ("\uE721", new SolidColorBrush(Color.FromRgb(0xFF, 0xB7, 0x4D))); // 검색 주황
if (lower.Contains("bash") || lower.Contains("script") || lower.Contains("run"))
return ("\uE756", new SolidColorBrush(Color.FromRgb(0xAB, 0x47, 0xBC))); // 실행 보라
if (lower.Contains("delete") || lower.Contains("remove"))
return ("\uE74D", new SolidColorBrush(Color.FromRgb(0xEF, 0x53, 0x50))); // 삭제 빨강
return ("\uE90F", new SolidColorBrush(Color.FromRgb(0x78, 0x90, 0x9C))); // 기타 회색
}
private static string GetV2ToolDisplayName(string? toolName)
{
if (string.IsNullOrWhiteSpace(toolName)) return "Tool";
return toolName switch
{
"file_read" => "Read file",
"document_read" => "Read document",
"file_write" => "Write file",
"file_edit" => "Edit file",
"web_search" => "Web search",
"bash" => "Run command",
"script_run" => "Run script",
"html_create" => "Create HTML",
"docx_create" => "Create DOCX",
"xlsx_create" or "excel_create" => "Create Excel",
"pptx_create" => "Create PPTX",
"csv_create" => "Create CSV",
"markdown_create" or "md_create" => "Create Markdown",
"context_compaction" => "Context compaction",
"agent_wait" => "Waiting",
_ => toolName,
};
}
private static string TruncateFilePath(string path, int maxLength)
{
if (string.IsNullOrEmpty(path) || path.Length <= maxLength)
return path;
var fileName = System.IO.Path.GetFileName(path);
if (fileName.Length >= maxLength - 3)
return "..." + fileName[^(maxLength - 3)..];
var remaining = maxLength - fileName.Length - 4; // ".../" 포함
if (remaining <= 0)
return ".../" + fileName;
return path[..remaining] + ".../" + fileName;
}
/// 긴 텍스트를 지정 길이로 잘라서 표시용으로 반환합니다.
private static string TruncateForDisplay(string text, int maxLength)
{
if (string.IsNullOrEmpty(text) || text.Length <= maxLength)
return text;
return text[..maxLength] + "\n… (truncated)";
}
}