< Summary

Line coverage
83%
Covered lines: 141
Uncovered lines: 28
Coverable lines: 169
Total lines: 366
Line coverage: 83.4%
Branch coverage
75%
Covered branches: 111
Total branches: 148
Branch coverage: 75%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: FunctionRegex()100%11100%
File 2: .ctor(...)100%11100%
File 2: UpdateSegments(...)80.43%514686.53%
File 2: FindOpeningTag(...)60%111077.77%
File 2: AppendToken(...)75%121287.5%
File 2: ProcessIncomingText(...)93.75%161693.75%
File 2: Close(...)75%5460%
File 2: Parse(...)68.33%916079.41%

File(s)

/home/runner/work/InvAit/InvAit/UIBlazor/obj/Release/net10.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs

File '/home/runner/work/InvAit/InvAit/UIBlazor/obj/Release/net10.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs' does not exist (any more).

/home/runner/work/InvAit/InvAit/UIBlazor/Services/MessageParser.cs

#LineLine coverage
 1using UIBlazor.Services.Settings;
 2
 3namespace UIBlazor.Services;
 4
 95public partial class MessageParser(IToolManager toolManager) : IMessageParser
 6{
 7    public void UpdateSegments(string delta, VisualChatMessage message, bool isHistory = false)
 8    {
 69        if (string.IsNullOrEmpty(delta))
 010            return;
 11
 812        var activeSegment = isHistory ? null : message.Segments.LastOrDefault(s => !s.IsClosed);
 13
 614        if (isHistory)
 15        {
 16            // В истории дельта - это целое неизменное сообщение.
 17            // Поэтому активный сегмент всегда новый для каждого вызова.
 018            activeSegment = null;
 19        }
 20
 621        var incomingText = delta;
 22
 1623        while (!string.IsNullOrEmpty(incomingText))
 24        {
 25            // Логика сегментов
 1026            if (activeSegment == null || activeSegment.IsClosed)
 27            {
 628                _rawAccumulator.Clear();
 629                activeSegment = new ContentSegment();
 630                message.Segments.Add(activeSegment);
 31            }
 32
 1033            var openIdx = FindOpeningTag(incomingText, out _);
 1034            var closeIdx = activeSegment.Type is not SegmentType.Unknown and not SegmentType.Markdown
 1035                           ? incomingText.IndexOf($"</{activeSegment.TagName}>")
 1036                           : -1;
 37
 38            // Сценарий А: Закрытие
 1039            if (closeIdx != -1 && (openIdx == -1 || closeIdx < openIdx))
 40            {
 341                var closingTag = $"</{activeSegment.TagName}>";
 342                var endOfTag = closeIdx + closingTag.Length;
 343                AppendToken(activeSegment, incomingText.StartsWith('\n')
 344                    ? incomingText[1..endOfTag] // убираем первый перенос строки после функции, если он есть
 345                    : incomingText[..endOfTag]);
 346                Close(activeSegment);
 347                incomingText = incomingText[endOfTag..];
 348                continue;
 49            }
 50
 51            // Сценарий Б: Открытие
 752            if (openIdx != -1)
 53            {
 454                if (openIdx > 0)
 55                {
 156                    AppendToken(activeSegment, incomingText[..openIdx]);
 157                    Close(activeSegment);
 158                    incomingText = incomingText[openIdx..];
 159                    continue;
 60                }
 361                else if (activeSegment is { IsClosed: false, Type: SegmentType.Markdown })
 62                {
 063                    Close(activeSegment);
 064                    continue;
 65                }
 66
 67                // Находим конец тега '>', чтобы знать, где кончаются параметры (name="...")
 368                var tagEndIdx = incomingText.IndexOf('>');
 369                if (tagEndIdx != -1)
 70                {
 371                    var consumptionLength = tagEndIdx + 1;
 372                    AppendToken(activeSegment, incomingText.Substring(0, consumptionLength));
 373                    if (activeSegment.Type == SegmentType.Tool && !string.IsNullOrEmpty(activeSegment.ToolName))
 74                    {
 375                        if (isHistory) // При загрузке истории все тулзы заапрувлены. Чтобы не было ложного Pending.
 76                        {
 077                            activeSegment.ApprovalStatus = ToolApprovalStatus.Approved;
 78                        }
 79                        else
 80                        {
 81                            // ну и сразу ставим статус, чтобы не было гонки между рендером и обновлением статуса после 
 382                            var mode = toolManager.GetApprovalModeByToolName(activeSegment.ToolName);
 383                            activeSegment.ApprovalStatus = mode switch
 384                            {
 085                                ToolApprovalMode.Ask => ToolApprovalStatus.Pending,
 086                                ToolApprovalMode.Deny => ToolApprovalStatus.Rejected,
 387                                _ => ToolApprovalStatus.Approved
 388                            };
 89                        }
 90                    }
 391                    incomingText = incomingText[consumptionLength..];
 392                    continue;
 93                }
 94            }
 95
 96            // Сценарий В: Обычный контент
 397            AppendToken(activeSegment, incomingText);
 398            incomingText = string.Empty;
 99        }
 6100    }
 101
 102    private static int FindOpeningTag(string text, out string tagName)
 103    {
 10104        tagName = "";
 10105        var planIdx = text.IndexOf("<plan");
 10106        var funcIdx = text.IndexOf("<function");
 107
 16108        if (planIdx == -1 && funcIdx == -1) return -1;
 109
 110        // Берем тот, что встретился раньше
 4111        if (planIdx != -1 && (funcIdx == -1 || planIdx < funcIdx))
 112        {
 0113            tagName = "plan";
 0114            return planIdx;
 115        }
 4116        tagName = "function";
 4117        return funcIdx;
 118    }
 119
 120    // Вспомогательный буфер для накопления "сырого" текста внутри сегмента
 9121    private readonly StringBuilder _rawAccumulator = new();
 122
 123    public void AppendToken(ContentSegment segment, string token)
 124    {
 10125        if (segment.IsClosed || string.IsNullOrEmpty(token)) return;
 126
 10127        _rawAccumulator.Append(token);
 128
 129        // 1. Определяем тип, если он еще не известен. Теги приходят полными
 10130        if (segment.Type == SegmentType.Unknown)
 131        {
 6132            var raw = _rawAccumulator.ToString();
 6133            var functionMatch = FunctionRegex().Match(token);
 6134            if (functionMatch.Success)
 135            {
 3136                segment.Type = SegmentType.Tool;
 3137                segment.TagName = "function";
 3138                segment.ToolName = functionMatch.Groups[1].Value;
 139            }
 3140            else if (raw.Contains("<plan>"))
 141            {
 0142                segment.Type = SegmentType.Plan;
 0143                segment.TagName = "plan";
 144            }
 3145            else if (!string.IsNullOrEmpty(raw))
 146            {
 3147                segment.Type = SegmentType.Markdown;
 148            }
 149        }
 150
 151        // 2. Обрабатываем текст и разбиваем на линии
 10152        ProcessIncomingText(segment, token);
 10153    }
 154
 155    private void ProcessIncomingText(ContentSegment segment, string token)
 156    {
 157        // Очищаем токен от управляющих тегов, чтобы парсеры их не видели
 10158        var cleanToken = token;
 10159        if (segment.Type != SegmentType.Markdown && !string.IsNullOrEmpty(segment.TagName))
 160        {
 161            // Удаляем <function...>, <plan...>, </function>, </plan>
 7162            cleanToken = Regex.Replace(token, $@".*<{segment.TagName}[^>]*>|<\/{segment.TagName}>.*", "", RegexOptions.N
 163        }
 164
 10165        if (string.IsNullOrEmpty(cleanToken) && token.Contains('>'))
 166        {
 4167            return;
 168        }
 169
 6170        segment.CurrentLine.Append(cleanToken);
 171
 172        // Если есть перенос строки - фиксируем завершенные линии
 6173        if (segment.Type != SegmentType.Markdown && cleanToken.Contains('\n'))
 174        {
 3175            var content = segment.CurrentLine.ToString();
 3176            var parts = content.Split('\n');
 177
 3178            if (!string.IsNullOrWhiteSpace(parts[0]))
 179            {
 0180                segment.Lines.Add(parts[0]);
 181            }
 182
 12183            for (var i = 1; i < parts.Length - 1; i++)
 184            {
 3185                segment.Lines.Add(parts[i]);
 186            }
 187
 3188            segment.CurrentLine.Clear();
 3189            segment.CurrentLine.Append(parts.Last());
 190        }
 6191    }
 192
 193    private static void Close(ContentSegment segment)
 194    {
 4195        segment.IsClosed = true;
 196        // Переносим остаток из буфера в финальные линии, если он там есть
 4197        if (segment.Type != SegmentType.Markdown && segment.CurrentLine.Length > 0)
 198        {
 0199            segment.Lines.Add(segment.CurrentLine.ToString());
 0200            segment.CurrentLine.Clear();
 201        }
 4202    }
 203
 204    public Dictionary<string, object> Parse(string toolName, List<string> toolLines)
 205    {
 5206        var result = new Dictionary<string, object>();
 5207        var paramIndex = 0;
 5208        var namedIndex = 0;
 209
 5210        if (toolName == BuiltInToolEnum.ReadFiles)
 211        {
 3212            ReadFileParams? fileParams = null;
 213
 20214            for (var i = 0; i < toolLines.Count; i++)
 215            {
 7216                var line = toolLines[i];
 7217                var trimmedLine = line.Trim();
 7218                if (string.IsNullOrEmpty(trimmedLine))
 219                    continue;
 220
 7221                if (trimmedLine == "start_line")
 222                {
 2223                    var valLine = toolLines[++i]?.Trim();
 2224                    if (fileParams != null && int.TryParse(valLine, out var startLine))
 225                    {
 2226                        fileParams.StartLine = startLine;
 227                    }
 228                }
 5229                else if (trimmedLine == "line_count")
 230                {
 1231                    var valLine = toolLines[++i]?.Trim();
 1232                    if (fileParams != null && int.TryParse(valLine, out var lineCount))
 233                    {
 1234                        fileParams.LineCount = lineCount;
 235                    }
 236                }
 237                else
 238                {
 4239                    fileParams = new ReadFileParams
 4240                    {
 4241                        Name = trimmedLine,
 4242                        StartLine = -1,
 4243                        LineCount = -1
 4244                    };
 4245                    result[$"file{++paramIndex}"] = fileParams;
 246                }
 247            }
 248        }
 2249        else if (toolName == BuiltInToolEnum.ApplyDiff)
 250        {
 22251            for (var i = 0; i < toolLines.Count; i++)
 252            {
 9253                var line = toolLines[i].Trim();
 254
 9255                if (string.IsNullOrEmpty(line))
 256                    continue;
 257
 258                // Начало блока (<<<<<<< SEARCH)
 9259                if (line.StartsWith("<<<<<<< SEARCH"))
 260                {
 4261                    i++;
 4262                    var diff = new DiffReplacement();
 4263                    var lastResult = result.LastOrDefault().Value?.ToString() ?? string.Empty;
 4264                    if (lastResult.StartsWith(":start_line:"))
 265                    {
 3266                        diff.StartLine = int.Parse(lastResult.Split(':')[2]);
 3267                        result.Remove($"param{paramIndex}");
 268                    }
 4269                    var search = new List<string>();
 16270                    for (; i < toolLines.Count; i++)
 271                    {
 10272                        line = toolLines[i].TrimEnd();
 10273                        if (line.StartsWith("======="))
 274                        {
 4275                            i++;
 4276                            break;
 277                        }
 6278                        search.Add(line);
 279                    }
 4280                    diff.Search = search;
 281
 4282                    var replace = new List<string>();
 20283                    for (; i < toolLines.Count; i++)
 284                    {
 12285                        line = toolLines[i].TrimEnd();
 12286                        if (line.StartsWith(">>>>>>> REPLACE"))
 287                        {
 4288                            i++;
 4289                            break;
 290                        }
 8291                        replace.Add(line);
 292                    }
 4293                    diff.Replace = replace;
 294
 4295                    result[$"diff{++namedIndex}"] = diff;
 296                }
 297                // Обычная строка параметров
 298                else
 299                {
 5300                    result[$"param{++paramIndex}"] = line;
 301                }
 302            }
 303        }
 0304        else if (toolName.StartsWith("mcp__")) // MCP
 305        {
 0306            for (var i = 0; i < toolLines.Count; i++)
 307            {
 0308                var line = toolLines[i];
 0309                var devider = line.IndexOf(':');
 0310                if (devider > 1)
 311                {
 0312                    var argName = line[..devider].Trim();
 0313                    var argValue = line[(devider + 1)..].Trim();
 314
 0315                    if (!string.IsNullOrEmpty(argName))
 316                    {
 0317                        if (argValue.Length > 2 && argValue.StartsWith('\"') && argValue.EndsWith('\"'))
 318                        {
 0319                            argValue = argValue[1..^1];
 320                        }
 0321                        result[argName] = argValue;
 322                        continue;
 323                    }
 324                }
 325            }
 326        }
 327        else // обычные тулзы
 328        {
 0329            for (var i = 0; i < toolLines.Count; i++)
 330            {
 0331                var line = toolLines[i];
 0332                result[$"param{++paramIndex}"] = line;
 333            }
 334        }
 335
 5336        return result;
 337    }
 338
 339    [GeneratedRegex(@"<function name=""([\w-_\.]+)"">$", RegexOptions.Compiled)]
 340    private static partial Regex FunctionRegex();
 341}