Forge
csharpdeeb25a2
1#nullable enable
2
3namespace CascadeIDE.Features.Chat;
4
5/// <summary>Разбор тела сообщения для Skia-ленты (кодовые блоки, thinking).</summary>
6public static class ChatMessageBodyPresentation
7{
8 public const string CollapsedThinkingPrefix = "[thinking свернут] ";
9
10 public static bool IsCollapsedThinking(string? body) =>
11 !string.IsNullOrEmpty(body)
12 && body.StartsWith(CollapsedThinkingPrefix, StringComparison.Ordinal);
13
14 public static bool CanToggleThinking(ChatMessageVisualRole role) =>
15 role == ChatMessageVisualRole.Thinking;
16
17 /// <summary>Разбить тело на prose и fenced code (```); все блоки кода в сообщении.</summary>
18 public static IReadOnlyList<ChatMessageBodySegment> SplitSegments(string? body)
19 {
20 if (string.IsNullOrEmpty(body))
21 return [new ChatMessageBodySegment(ChatMessageBodySegmentKind.Prose, "")];
22
23 var segments = new List<ChatMessageBodySegment>();
24 var index = 0;
25 while (index < body.Length)
26 {
27 var fenceStart = body.IndexOf("```", index, StringComparison.Ordinal);
28 if (fenceStart < 0)
29 {
30 appendProseTail(segments, body[index..]);
31 break;
32 }
33
34 if (fenceStart > index)
35 appendProseTail(segments, body[index..fenceStart]);
36
37 var afterFence = fenceStart + 3;
38 var lineEnd = body.IndexOf('\n', afterFence);
39 if (lineEnd < 0)
40 {
41 appendProseTail(segments, body[fenceStart..]);
42 break;
43 }
44
45 var codeStart = lineEnd + 1;
46 var endFence = body.IndexOf("```", codeStart, StringComparison.Ordinal);
47 if (endFence < 0)
48 {
49 appendProseTail(segments, body[fenceStart..]);
50 break;
51 }
52
53 var code = body[codeStart..endFence].TrimEnd('\r', '\n');
54 if (code.Length > 0)
55 segments.Add(new ChatMessageBodySegment(ChatMessageBodySegmentKind.Code, code));
56
57 index = endFence + 3;
58 }
59
60 return segments.Count == 0
61 ? [new ChatMessageBodySegment(ChatMessageBodySegmentKind.Prose, body)]
62 : segments;
63 }
64
65 /// <summary>Prose с блочной разметкой (заголовки, списки) — document layout в ленте.</summary>
66 public static bool ShouldUseDocumentLayout(string? body)
67 {
68 if (string.IsNullOrWhiteSpace(body))
69 return false;
70
71 foreach (var rawLine in body.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n'))
72 {
73 var line = rawLine.Trim();
74 if (line.Length == 0)
75 continue;
76
77 if (line.StartsWith("### ", StringComparison.Ordinal)
78 || line.StartsWith("## ", StringComparison.Ordinal)
79 || line.StartsWith("# ", StringComparison.Ordinal)
80 || line.StartsWith("- ", StringComparison.Ordinal)
81 || line.StartsWith("* ", StringComparison.Ordinal))
82 {
83 return true;
84 }
85
86 if (line.Length >= 3
87 && line.All(c => c is '-' or '*' or ' ' or '_')
88 && line.Any(c => c is '-' or '*'))
89 {
90 return true;
91 }
92 }
93
94 return false;
95 }
96
97 private static void appendProseTail(List<ChatMessageBodySegment> segments, string text)
98 {
99 var prose = text.TrimEnd();
100 if (prose.Length > 0)
101 segments.Add(new ChatMessageBodySegment(ChatMessageBodySegmentKind.Prose, prose));
102 }
103}
104
105public enum ChatMessageBodySegmentKind
106{
107 Prose = 0,
108 Code = 1,
109}
110
111public readonly record struct ChatMessageBodySegment(ChatMessageBodySegmentKind Kind, string Text);
112
View only · write via MCP/CIDE