diff --git a/app/AGENTS.md b/app/AGENTS.md index 655417bc9..0ccf21c0f 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -29,6 +29,7 @@ Key structure: - **Naming conventions** - Constants, enum members, and `static readonly` fields use `UPPER_SNAKE_CASE` such as `MY_CONSTANT`. - **Extension members** - Declare extension methods in an `extension` block of their static class, one block per receiver, never with a `this` parameter: MWAIS0016 refuses the classic form. The `` of the receiver, and of a generic block its ``, belongs on the block, not on its members. A member which only looks something up about its receiver, such as a name, an icon, a flag, a prompt, or an address, is an extension property, named without `Get` or `To`: `format.FileExtension`, `host.IsChatSupported`. It stays a method when it takes parameters, builds something new such as a list or a model profile, parses, scans a collection, or converts into another type, as `GetModelProfile`, `CompactCount`, and `ToSources` do. Check first that the receiver has no instance member of the same name, which would hide the extension. A helper which belongs to the type rather than to one of its values is a static member of the block and is reached through the type: `RID.Current`, `FileExportFormat.TryFromCodeFenceLanguage`. Never name one `Parse` or `TryParse`: C# finds those of `Enum` first. +- **Line breaks** - Every text AI Studio builds ends its lines with LF, on every system: use `AppendLineLF` of `SharedTools.Text` instead of `StringBuilder.AppendLine`, and `'\n'` instead of `Environment.NewLine`. Both of them write CR LF on Windows, where no pipeline runs the tests; MWAIS0018 refuses them, and its code fix offers the replacement. Where a format needs CR LF, write it on purpose, as `CsvWriter.ROW_END` does. A raw string literal and a file which comes with the app keep the line endings of the checkout, CR LF on Windows with `core.autocrlf`: a test which compares such a text with what the app built normalizes it first, e.g. with `ReplaceLineEndings("\n")`. - **Message Bus** - Singleton event bus for cross-component communication inside the .NET app - **Prompt injection reports** - The user hears once per action about what the filter removed, as soon as that action ends. Content which belongs to an action carries its scope from `PromptInjectionGuardService.BeginAction` down to where it is read or filtered; whatever is filtered without a scope is reported right away. Pass the scope as a parameter, never through a field or an `AsyncLocal`: the service serves the whole app, and the message bus delivers to other receivers in the execution context of whoever sends. - **Paths in stored data** - A stored path to a file which AI Studio keeps below the data directory, such as a transcript or a tokenizer copy, is relative to that directory, so that a backup or a copy of the data directory works on another computer, under another user name, or on another OS. Mark such a property with `[JsonConverter(typeof(DataDirectoryRelativePathConverter))]`, for a positional record parameter with `[property: ...]`; in memory, the path stays absolute. Better still, derive the path from IDs and store none, as `VisualBriefingStore` does. Paths to files of the user stay absolute. Chats stored before v26.10.1 are migrated by a compatibility shim, see `documentation/compatibility-shims/2026-10-relative-data-paths.md`. diff --git a/app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs b/app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs index 0c5a6b236..c81078dac 100644 --- a/app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs +++ b/app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs @@ -190,7 +190,7 @@ public sealed class AgentDataSourceSelection (ILogger logger.LogInformation("Preparing the list of allowed data sources for the agent to choose from."); var sb = new StringBuilder(); - sb.AppendLine("The following data sources are available for selection:"); + sb.AppendLineLF("The following data sources are available for selection:"); foreach (var ds in dataSources.AllowedDataSources) { var description = await descriptionService.GetDescriptionAsync(ds, token); @@ -198,15 +198,15 @@ public sealed class AgentDataSourceSelection (ILogger switch (ds) { case DataSourceLocalDirectory localDirectory: - sb.AppendLine($"- Id={ds.Id}, name='{localDirectory.Name}', type=local directory, path='{localDirectory.Path}'{descriptionPart}"); + sb.AppendLineLF($"- Id={ds.Id}, name='{localDirectory.Name}', type=local directory, path='{localDirectory.Path}'{descriptionPart}"); break; case DataSourceLocalFile localFile: - sb.AppendLine($"- Id={ds.Id}, name='{localFile.Name}', type=local file, path='{localFile.FilePath}'{descriptionPart}"); + sb.AppendLineLF($"- Id={ds.Id}, name='{localFile.Name}', type=local file, path='{localFile.FilePath}'{descriptionPart}"); break; case IERIDataSource eriDataSource: - sb.AppendLine($"- Id={ds.Id}, name='{eriDataSource.Name}', type=external data source{descriptionPart}"); + sb.AppendLineLF($"- Id={ds.Id}, name='{eriDataSource.Name}', type=external data source{descriptionPart}"); break; } } diff --git a/app/MindWork AI Studio/Agents/AssistantAudit/AssistantAuditAgent.cs b/app/MindWork AI Studio/Agents/AssistantAudit/AssistantAuditAgent.cs index 22a822127..1f5398fd7 100644 --- a/app/MindWork AI Studio/Agents/AssistantAudit/AssistantAuditAgent.cs +++ b/app/MindWork AI Studio/Agents/AssistantAudit/AssistantAuditAgent.cs @@ -101,7 +101,7 @@ public sealed class AssistantAuditAgent(ILogger logger, ILo protected override string SystemPrompt(string additionalData) => string.IsNullOrWhiteSpace(additionalData) ? this.JobDescription - : $"{this.JobDescription}{Environment.NewLine}{Environment.NewLine}{additionalData}"; + : $"{this.JobDescription}\n\n{additionalData}"; public override AIStudio.Settings.Provider ProviderSettings { get; set; } = AIStudio.Settings.Provider.NONE; @@ -343,11 +343,11 @@ public sealed class AssistantAuditAgent(ILogger logger, ILo var definition = toolRegistry.GetDefinition(toolId); if (definition is null) { - builder.AppendLine($"- {toolId}: unknown to this installation"); + builder.AppendLineLF($"- {toolId}: unknown to this installation"); continue; } - builder.AppendLine($"- {toolId}: {definition.Function.DescriptionForLLM}"); + builder.AppendLineLF($"- {toolId}: {definition.Function.DescriptionForLLM}"); } return builder.ToString().TrimEnd(); @@ -368,11 +368,11 @@ public sealed class AssistantAuditAgent(ILogger logger, ILo foreach (var luaFile in luaFiles.OrderBy(file => file.Key, StringComparer.Ordinal)) { if (builder.Length > 0) - builder.AppendLine().AppendLine(); + builder.AppendLineLF().AppendLineLF(); builder.Append("-- File: "); - builder.AppendLine(luaFile.Key); - builder.AppendLine(luaFile.Value); + builder.AppendLineLF(luaFile.Key); + builder.AppendLineLF(luaFile.Value); } return builder.ToString().TrimEnd(); diff --git a/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs b/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs index bd21d43da..37107f817 100644 --- a/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs +++ b/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs @@ -618,7 +618,7 @@ public partial class AssistantAgenda : AssistantBaseCore { var markdown = new StringBuilder(); foreach (var item in items) - markdown.AppendLine($"- {item}"); + markdown.AppendLineLF($"- {item}"); return markdown.ToString(); } diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs index 69b00b88e..06e88d1c4 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs @@ -1090,7 +1090,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher if (outcome.Failures.Count > 0) { - var message = string.Join(Environment.NewLine, outcome.Failures.Select(failure => $"{failure.FileName}: {failure.UserMessage}")); + var message = string.Join('\n',outcome.Failures.Select(failure => $"{failure.FileName}: {failure.UserMessage}")); await this.MessageBus.SendError(new(AppIcons.VOICE_TRANSCRIPTION, message)); } else if (outcome.Status is MediaImportStatus.FAILED) @@ -1100,7 +1100,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher if (outcome.Warnings.Count > 0) { - var message = string.Join(Environment.NewLine, outcome.Warnings.Select(warning => $"{warning.FileName}: {warning.UserMessage}")); + var message = string.Join('\n',outcome.Warnings.Select(warning => $"{warning.FileName}: {warning.UserMessage}")); await this.MessageBus.SendWarning(new(AppIcons.VOICE_TRANSCRIPTION, message)); } diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Persistence.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Persistence.cs index 745ac4463..0ab3d2be2 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Persistence.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Persistence.cs @@ -106,9 +106,9 @@ public partial class AssistantBatchProcessing private async Task WriteLogAsync(string resolvedOutputDirectory) { var sb = new StringBuilder(); - sb.AppendLine(CsvWriter.ToRow(LOG_SEPARATOR, T("File"), T("Time"), T("Model"), T("Status"), T("Details"), T("Tools used"))); + sb.Append(CsvWriter.ToRow(LOG_SEPARATOR, T("File"), T("Time"), T("Model"), T("Status"), T("Details"), T("Tools used"))).Append(CsvWriter.ROW_END); foreach (var fileResult in this.fileResults.Where(x => x.Status is not BatchProcessingFileStatus.QUEUED and not BatchProcessingFileStatus.PROCESSING)) - sb.AppendLine(CsvWriter.ToRow(LOG_SEPARATOR, fileResult.RelativePath, fileResult.ProcessedAt.ToString(TIME_FORMAT, CultureInfo.InvariantCulture), fileResult.ModelName, fileResult.Status.ToString(), fileResult.Message, fileResult.UsedTools)); + sb.Append(CsvWriter.ToRow(LOG_SEPARATOR, fileResult.RelativePath, fileResult.ProcessedAt.ToString(TIME_FORMAT, CultureInfo.InvariantCulture), fileResult.ModelName, fileResult.Status.ToString(), fileResult.Message, fileResult.UsedTools)).Append(CsvWriter.ROW_END); await this.WriteCsvFileAsync(Path.Join(resolvedOutputDirectory, LOG_FILENAME), sb.ToString()); } @@ -120,9 +120,9 @@ public partial class AssistantBatchProcessing { var separator = this.csvSeparator.Character(this.customCsvSeparator); var sb = new StringBuilder(); - sb.AppendLine(CsvWriter.ToRow(separator, T("File"), this.ResultColumnHeader)); + sb.Append(CsvWriter.ToRow(separator, T("File"), this.ResultColumnHeader)).Append(CsvWriter.ROW_END); foreach (var fileResult in this.fileResults.Where(x => x.Status is BatchProcessingFileStatus.DONE)) - sb.AppendLine(CsvWriter.ToRow(separator, fileResult.RelativePath, fileResult.ResultText)); + sb.Append(CsvWriter.ToRow(separator, fileResult.RelativePath, fileResult.ResultText)).Append(CsvWriter.ROW_END); await this.WriteCsvFileAsync(Path.Join(resolvedOutputDirectory, this.ResolveResultsFileName()), sb.ToString()); } diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflReflectionFolder.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflReflectionFolder.cs index f6ac62ab5..db82017ab 100644 --- a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflReflectionFolder.cs +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflReflectionFolder.cs @@ -2,8 +2,6 @@ using System.Text; using AIStudio.Chat; -using SharedTools; - namespace AIStudio.Assistants.BeyondFactsLab; /// diff --git a/app/MindWork AI Studio/Assistants/BiasDay/BiasOfTheDayAssistant.razor.cs b/app/MindWork AI Studio/Assistants/BiasDay/BiasOfTheDayAssistant.razor.cs index a57b16fc7..7f9ff8f3b 100644 --- a/app/MindWork AI Studio/Assistants/BiasDay/BiasOfTheDayAssistant.razor.cs +++ b/app/MindWork AI Studio/Assistants/BiasDay/BiasOfTheDayAssistant.razor.cs @@ -111,12 +111,12 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore 0) { - sb.AppendLine(); - sb.AppendLine("Please share the following sources with the user as a Markdown list:"); + sb.AppendLineLF(); + sb.AppendLineLF("Please share the following sources with the user as a Markdown list:"); foreach (var link in this.biasOfTheDay.Links) - sb.AppendLine($"- {link}"); + sb.AppendLineLF($"- {link}"); - sb.AppendLine(); + sb.AppendLineLF(); } return sb.ToString(); diff --git a/app/MindWork AI Studio/Assistants/Coding/AssistantCoding.razor.cs b/app/MindWork AI Studio/Assistants/Coding/AssistantCoding.razor.cs index 457ed934b..1c3610a4e 100644 --- a/app/MindWork AI Studio/Assistants/Coding/AssistantCoding.razor.cs +++ b/app/MindWork AI Studio/Assistants/Coding/AssistantCoding.razor.cs @@ -197,12 +197,12 @@ public partial class AssistantCoding : AssistantBaseCore var sbCompilerMessages = new StringBuilder(); if (this.provideCompilerMessages) { - sbCompilerMessages.AppendLine("I have the following compiler messages:"); - sbCompilerMessages.AppendLine(); - sbCompilerMessages.AppendLine("```"); - sbCompilerMessages.AppendLine(this.compilerMessages); - sbCompilerMessages.AppendLine("```"); - sbCompilerMessages.AppendLine(); + sbCompilerMessages.AppendLineLF("I have the following compiler messages:"); + sbCompilerMessages.AppendLineLF(); + sbCompilerMessages.AppendLineLF("```"); + sbCompilerMessages.AppendLineLF(this.compilerMessages); + sbCompilerMessages.AppendLineLF("```"); + sbCompilerMessages.AppendLineLF(); } this.CreateChatThread(); diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs index b8ef9d9bf..2a21fad08 100644 --- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs +++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs @@ -897,7 +897,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore 0) { - sb.AppendLine(""" + sb.AppendLineLF(""" # DOCUMENTS: """); @@ -941,7 +941,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore var componentSummary = this.assistantPlugin.CreateAuditComponentSummary(); if (!string.IsNullOrWhiteSpace(componentSummary)) { - builder.AppendLine("Current component overview:"); - builder.AppendLine(componentSummary); - builder.AppendLine(); + builder.AppendLineLF("Current component overview:"); + builder.AppendLineLF(componentSummary); + builder.AppendLineLF(); } } var promptPreview = await this.CollectUserPromptAsync(); if (!string.IsNullOrWhiteSpace(promptPreview)) { - builder.AppendLine("Current prompt preview from the assistant form:"); - builder.AppendLine(promptPreview); - builder.AppendLine(); + builder.AppendLineLF("Current prompt preview from the assistant form:"); + builder.AppendLineLF(promptPreview); + builder.AppendLineLF(); } if (this.ResultingContentBlock?.Content is ContentText text && !string.IsNullOrWhiteSpace(text.Text)) { - builder.AppendLine("Last assistant response visible in this session:"); - builder.AppendLine(text.Text); + builder.AppendLineLF("Last assistant response visible in this session:"); + builder.AppendLineLF(text.Text); } return builder.ToString().Trim(); @@ -740,6 +740,6 @@ public partial class AssistantDynamic : AssistantBaseCore } } - return prompt.Append(Environment.NewLine).ToString(); + return prompt.AppendLineLF().ToString(); } } diff --git a/app/MindWork AI Studio/Assistants/EMail/AssistantEMail.razor.cs b/app/MindWork AI Studio/Assistants/EMail/AssistantEMail.razor.cs index c91977853..069496b66 100644 --- a/app/MindWork AI Studio/Assistants/EMail/AssistantEMail.razor.cs +++ b/app/MindWork AI Studio/Assistants/EMail/AssistantEMail.razor.cs @@ -256,9 +256,9 @@ public partial class AssistantEMail : AssistantBaseCore $"- Picture {firstNumber + index}: {picture.Name}{(picture.IsIcon ? " (icon)" : string.Empty)}")); + var list = string.Join('\n',library.Pictures.Select((picture, index) => $"- Picture {firstNumber + index}: {picture.Name}{(picture.IsIcon ? " (icon)" : string.Empty)}")); var sanitizedList = await guardService.SanitizeAsync(list, PromptInjectionSource.FileContent(folderPath), promptInjectionScope); return $""" diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor.Presentation.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor.Presentation.cs index d8fdab288..fffe2c9fe 100644 --- a/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor.Presentation.cs +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor.Presentation.cs @@ -357,7 +357,7 @@ public partial class SlideAssistant if (this.SourceImagesOfRequest().Count > 0) rules.Add(" - Images attached as source material are for reading only. Never place them on a slide."); - return string.Join(Environment.NewLine, rules); + return string.Join('\n',rules); } /// @@ -376,7 +376,7 @@ public partial class SlideAssistant if (pictures.Count == 0) return string.Empty; - var list = string.Join(Environment.NewLine, pictures.Select((picture, index) => $"- Picture {index + 1}: {picture.FileName}")); + var list = string.Join('\n',pictures.Select((picture, index) => $"- Picture {index + 1}: {picture.FileName}")); var seeing = modelSeesPictures ? "The images attached to this request start with these pictures, in this order." : "You cannot see these pictures. Place them by what their names say."; @@ -516,7 +516,7 @@ public partial class SlideAssistant private static string RepairPromptOf(IReadOnlyList issues) { - var listedIssues = string.Join(Environment.NewLine, issues.Take(MAX_REPORTED_ISSUES).Select(issue => $"- {issue}")); + var listedIssues = string.Join('\n',issues.Take(MAX_REPORTED_ISSUES).Select(issue => $"- {issue}")); return $""" Your JSON does not fit the layouts yet: {listedIssues} diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor.cs b/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor.cs index 0aeb76481..d36cef4bc 100644 --- a/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor.cs +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor.cs @@ -542,7 +542,7 @@ public partial class SlideAssistant : AssistantBaseCore $" - {prompt}")); + return string.Join('\n',prompts.Select(prompt => $" - {prompt}")); } private string GetDocumentTaskDescription() @@ -577,7 +577,7 @@ public partial class SlideAssistant : AssistantBaseCore 0) { - sb.AppendLine(""" + sb.AppendLineLF(""" # DOCUMENTS: """); @@ -614,7 +614,7 @@ public partial class SlideAssistant : AssistantBaseCore 0) - await this.MessageBus.SendError(new(AppIcons.VOICE_TRANSCRIPTION, string.Join(Environment.NewLine, outcome.Failures.Select(failure => $"{failure.FileName}: {failure.UserMessage}")))); + await this.MessageBus.SendError(new(AppIcons.VOICE_TRANSCRIPTION, string.Join('\n',outcome.Failures.Select(failure => $"{failure.FileName}: {failure.UserMessage}")))); else if (outcome.Status is MediaImportStatus.FAILED) await this.MessageBus.SendError(new(AppIcons.VOICE_TRANSCRIPTION, T("The media file could not be transcribed."))); if (outcome.Warnings.Count > 0) - await this.MessageBus.SendWarning(new(AppIcons.VOICE_TRANSCRIPTION, string.Join(Environment.NewLine, outcome.Warnings.Select(warning => $"{warning.FileName}: {warning.UserMessage}")))); + await this.MessageBus.SendWarning(new(AppIcons.VOICE_TRANSCRIPTION, string.Join('\n',outcome.Warnings.Select(warning => $"{warning.FileName}: {warning.UserMessage}")))); if (outcome.Status is MediaImportStatus.CANCELLED) await this.MessageBus.SendWarning(new(AppIcons.VOICE_TRANSCRIPTION, T("The media transcription was canceled."))); diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingStructuredResponseProcessor.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingStructuredResponseProcessor.cs index d082a0577..148e70ede 100644 --- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingStructuredResponseProcessor.cs +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingStructuredResponseProcessor.cs @@ -78,7 +78,7 @@ internal static partial class VisualBriefingStructuredResponseProcessor var root = GetContract(typeof(T)); var shapes = EnumerateObjectShapes(root); var builder = new StringBuilder(); - builder.AppendLine("Strict JSON grammar generated from the active response contract:"); + builder.AppendLineLF("Strict JSON grammar generated from the active response contract:"); foreach (var shape in shapes) { builder.Append(shape.Name); @@ -98,7 +98,7 @@ internal static partial class VisualBriefingStructuredResponseProcessor if (property.AllowsNull) builder.Append(" | null"); } - builder.AppendLine("}"); + builder.AppendLineLF("}"); } builder.Append( "Every object may contain only the properties shown above. Required properties must be present even when their value is null."); diff --git a/app/MindWork AI Studio/Chat/ContentExtensions.cs b/app/MindWork AI Studio/Chat/ContentExtensions.cs index d5a95caeb..23f9e12f0 100644 --- a/app/MindWork AI Studio/Chat/ContentExtensions.cs +++ b/app/MindWork AI Studio/Chat/ContentExtensions.cs @@ -108,7 +108,7 @@ public static class ContentExtensions if (sources.Length is 0 || !file.Format.TryToComment(sources, out var comment)) return file.Content; - return $"{file.Content}{Environment.NewLine}{Environment.NewLine}{comment}"; + return $"{file.Content}\n\n{comment}"; } } diff --git a/app/MindWork AI Studio/Chat/ContentText.cs b/app/MindWork AI Studio/Chat/ContentText.cs index e224f0e3e..f3a77a25b 100644 --- a/app/MindWork AI Studio/Chat/ContentText.cs +++ b/app/MindWork AI Studio/Chat/ContentText.cs @@ -775,7 +775,7 @@ public sealed class ContentText : IContent { var sb = new StringBuilder(); var attachmentWebAddresses = new HashSet(StringComparer.Ordinal); - sb.AppendLine(this.Text); + sb.AppendLineLF(this.Text); if(this.FileAttachments.Count > 0) { @@ -856,13 +856,13 @@ public sealed class ContentText : IContent content = extraction.Content; } - documentBlocks.AppendLine(); - documentBlocks.AppendLine("---------------------------------------"); - documentBlocks.AppendLine($"File path: {document.FilePath}"); - documentBlocks.AppendLine("File content:"); - documentBlocks.AppendLine("````"); - documentBlocks.AppendLine(content); - documentBlocks.AppendLine("````"); + documentBlocks.AppendLineLF(); + documentBlocks.AppendLineLF("---------------------------------------"); + documentBlocks.AppendLineLF($"File path: {document.FilePath}"); + documentBlocks.AppendLineLF("File content:"); + documentBlocks.AppendLineLF("````"); + documentBlocks.AppendLineLF(content); + documentBlocks.AppendLineLF("````"); foreach (var address in WebAddresses.Find(content)) if (WebAddresses.TryCreateRequestKey(address, out var requestKey)) @@ -871,17 +871,17 @@ public sealed class ContentText : IContent if (documentBlocks.Length > 0) { - sb.AppendLine(); - sb.AppendLine("The following files are attached to this message:"); + sb.AppendLineLF(); + sb.AppendLineLF("The following files are attached to this message:"); sb.Append(documentBlocks); } var numImages = normalizedAttachments.Count(x => x is { IsImage: true, Exists: true }); if (numImages > 0) { - sb.AppendLine(); - sb.AppendLine($"Additionally, there are {numImages} image file(s) attached to this message. "); - sb.AppendLine("Please consider them as part of the message content and use them to answer accordingly."); + sb.AppendLineLF(); + sb.AppendLineLF($"Additionally, there are {numImages} image file(s) attached to this message. "); + sb.AppendLineLF("Please consider them as part of the message content and use them to answer accordingly."); } } diff --git a/app/MindWork AI Studio/Chat/MarkdownRenderPlanner.cs b/app/MindWork AI Studio/Chat/MarkdownRenderPlanner.cs index 1f2754df9..a04223212 100644 --- a/app/MindWork AI Studio/Chat/MarkdownRenderPlanner.cs +++ b/app/MindWork AI Studio/Chat/MarkdownRenderPlanner.cs @@ -171,7 +171,7 @@ internal static class MarkdownRenderPlanner return cleaned; if (LooksLikeRawHtml(cleaned)) - return $"{HTML_CODE_FENCE_PREFIX}{Environment.NewLine}{cleaned}{Environment.NewLine}{CODE_FENCE_MARKER_BACKTICK}"; + return $"{HTML_CODE_FENCE_PREFIX}\n{cleaned}\n{CODE_FENCE_MARKER_BACKTICK}"; return cleaned; } diff --git a/app/MindWork AI Studio/Chat/MathJaxBlock.razor.cs b/app/MindWork AI Studio/Chat/MathJaxBlock.razor.cs index 4254352da..92164efbf 100644 --- a/app/MindWork AI Studio/Chat/MathJaxBlock.razor.cs +++ b/app/MindWork AI Studio/Chat/MathJaxBlock.razor.cs @@ -38,5 +38,5 @@ public partial class MathJaxBlock ? "formula-block" : $"formula-block {this.Class}"; - private string MathText => $"$${Environment.NewLine}{this.Value}{Environment.NewLine}$$"; + private string MathText => $"$$\n{this.Value}\n$$"; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs index 75f05f2cc..fe6c856e2 100644 --- a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs +++ b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs @@ -171,7 +171,7 @@ public partial class AttachDocuments : MSGComponentBase if (outcome.Failures.Count > 0) { - var message = string.Join(Environment.NewLine, outcome.Failures.Select(failure => $"{failure.FileName}: {failure.UserMessage}")); + var message = string.Join('\n',outcome.Failures.Select(failure => $"{failure.FileName}: {failure.UserMessage}")); await this.MessageBus.SendError(new(AppIcons.VOICE_TRANSCRIPTION, message)); } else if (outcome.Status is MediaImportStatus.FAILED) @@ -181,7 +181,7 @@ public partial class AttachDocuments : MSGComponentBase if (outcome.Warnings.Count > 0) { - var message = string.Join(Environment.NewLine, outcome.Warnings.Select(warning => $"{warning.FileName}: {warning.UserMessage}")); + var message = string.Join('\n',outcome.Warnings.Select(warning => $"{warning.FileName}: {warning.UserMessage}")); await this.MessageBus.SendWarning(new(AppIcons.VOICE_TRANSCRIPTION, message)); } diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs index 779f1a7fd..cbbc855f6 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs +++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs @@ -532,7 +532,7 @@ public partial class ChatComponent : MSGComponentBase if (outcome.Failures.Count > 0) { - var message = string.Join(Environment.NewLine, outcome.Failures.Select(failure => $"{failure.FileName}: {failure.UserMessage}")); + var message = string.Join('\n',outcome.Failures.Select(failure => $"{failure.FileName}: {failure.UserMessage}")); await this.MessageBus.SendError(new(AppIcons.VOICE_TRANSCRIPTION, message)); } else if (outcome.Status is MediaImportStatus.FAILED) @@ -542,7 +542,7 @@ public partial class ChatComponent : MSGComponentBase if (outcome.Warnings.Count > 0) { - var message = string.Join(Environment.NewLine, outcome.Warnings.Select(warning => $"{warning.FileName}: {warning.UserMessage}")); + var message = string.Join('\n',outcome.Warnings.Select(warning => $"{warning.FileName}: {warning.UserMessage}")); await this.MessageBus.SendWarning(new(AppIcons.VOICE_TRANSCRIPTION, message)); } diff --git a/app/MindWork AI Studio/Components/ReadFileContent.razor.cs b/app/MindWork AI Studio/Components/ReadFileContent.razor.cs index 55ff7882a..0f3bdbc6d 100644 --- a/app/MindWork AI Studio/Components/ReadFileContent.razor.cs +++ b/app/MindWork AI Studio/Components/ReadFileContent.razor.cs @@ -143,7 +143,7 @@ public partial class ReadFileContent : MSGComponentBase if (outcome.Failures.Count > 0) { - var message = string.Join(Environment.NewLine, outcome.Failures.Select(failure => $"{failure.FileName}: {failure.UserMessage}")); + var message = string.Join('\n',outcome.Failures.Select(failure => $"{failure.FileName}: {failure.UserMessage}")); await this.MessageBus.SendError(new(AppIcons.VOICE_TRANSCRIPTION, message)); } else if (outcome.Status is MediaImportStatus.FAILED) @@ -153,7 +153,7 @@ public partial class ReadFileContent : MSGComponentBase if (outcome.Warnings.Count > 0) { - var message = string.Join(Environment.NewLine, outcome.Warnings.Select(warning => $"{warning.FileName}: {warning.UserMessage}")); + var message = string.Join('\n',outcome.Warnings.Select(warning => $"{warning.FileName}: {warning.UserMessage}")); await this.MessageBus.SendWarning(new(AppIcons.VOICE_TRANSCRIPTION, message)); } diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs index a4fc2beb7..3f69e221a 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs @@ -153,7 +153,7 @@ public partial class SettingsPanelApp : SettingsPanelBase private string GetExternalHttpCustomRootCertificateAllowedHostsText() { - return string.Join(Environment.NewLine, this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts.Order(StringComparer.OrdinalIgnoreCase)); + return string.Join('\n',this.SettingsManager.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts.Order(StringComparer.OrdinalIgnoreCase)); } private bool AreExternalHttpCustomRootCertificateDetailsDisabled() diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs index 953f59900..196ea789b 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs @@ -118,7 +118,7 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase // var dialogParameters = string.IsNullOrEmpty(affectedDataSources) ? new DialogParameters { { x => x.Message, question } } - : new DialogParameters { { x => x.MarkdownBody, $"{affectedDataSources}{Environment.NewLine}{question}" } }; + : new DialogParameters { { x => x.MarkdownBody, $"{affectedDataSources}\n{question}" } }; var dialogReference = await this.DialogService.ShowAsync(T("Delete Embedding Provider"), dialogParameters, DialogOptions.FULLSCREEN); var dialogResult = await dialogReference.Result; @@ -227,7 +227,7 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase return; } - var resultText = string.Join(Environment.NewLine, vector.Select(value => value.ToString("G9", CultureInfo.InvariantCulture))); + var resultText = string.Join('\n',vector.Select(value => value.ToString("G9", CultureInfo.InvariantCulture))); var resultParameters = new DialogParameters { { x => x.ResultText, resultText }, diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1InfoDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1InfoDialog.razor.cs index 107874585..8f1e15b2c 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1InfoDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1InfoDialog.razor.cs @@ -103,9 +103,9 @@ public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, ISecretId foreach (var (paramName, description) in parameters) { sb.Append(T("Parameter: ")); - sb.AppendLine(paramName); - sb.AppendLine(description); - sb.AppendLine(); + sb.AppendLineLF(paramName); + sb.AppendLineLF(description); + sb.AppendLineLF(); } return sb.ToString(); diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs index 6fcabb6d8..6184138e5 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs @@ -86,7 +86,7 @@ public partial class DataSourceLocalDirectoryInfoDialog : MSGComponentBase private void UpdateFileList(string file) { this.directoryFiles.Append("- "); - this.directoryFiles.AppendLine(file); + this.directoryFiles.AppendLineLF(file); this.directoryFilesText = this.directoryFiles.ToString(); } diff --git a/app/MindWork AI Studio/GlobalUsings.cs b/app/MindWork AI Studio/GlobalUsings.cs index 1ee3615ef..2e602a2c7 100644 --- a/app/MindWork AI Studio/GlobalUsings.cs +++ b/app/MindWork AI Studio/GlobalUsings.cs @@ -5,4 +5,6 @@ global using AIStudio.Tools; global using Microsoft.JSInterop; global using MudBlazor; -global using MudExtensions; \ No newline at end of file +global using MudExtensions; + +global using SharedTools.Text; \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Anthropic/AnthropicToolCallingAdapter.cs b/app/MindWork AI Studio/Provider/Anthropic/AnthropicToolCallingAdapter.cs index 851a49f8f..4df6427dc 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/AnthropicToolCallingAdapter.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/AnthropicToolCallingAdapter.cs @@ -47,7 +47,7 @@ public sealed class AnthropicToolCallingAdapter(Model chatModel, IList( var requestSystemPrompt = finalResponseInstruction is null ? systemPrompt : systemPrompt with { - Content = $"{systemPrompt.Content}{Environment.NewLine}{Environment.NewLine}{finalResponseInstruction}", + Content = $"{systemPrompt.Content}\n\n{finalResponseInstruction}", }; ChatCompletionAPIRequest requestDtoBase = await requestFactory(requestSystemPrompt, apiParameters, includeTools ? providerTools : null); diff --git a/app/MindWork AI Studio/Provider/OpenAI/ResponsesToolCallingAdapter.cs b/app/MindWork AI Studio/Provider/OpenAI/ResponsesToolCallingAdapter.cs index f8dbae938..8bda6d725 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ResponsesToolCallingAdapter.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ResponsesToolCallingAdapter.cs @@ -37,7 +37,7 @@ public sealed class ResponsesToolCallingAdapter(Model chatModel, IList b { requestInput[0] = systemPrompt with { - Content = $"{systemPrompt.Content}{Environment.NewLine}{Environment.NewLine}{finalResponseInstruction}", + Content = $"{systemPrompt.Content}\n\n{finalResponseInstruction}", }; } diff --git a/app/MindWork AI Studio/Settings/ChatTemplate.cs b/app/MindWork AI Studio/Settings/ChatTemplate.cs index 3affa0762..e4b22cd34 100644 --- a/app/MindWork AI Studio/Settings/ChatTemplate.cs +++ b/app/MindWork AI Studio/Settings/ChatTemplate.cs @@ -658,7 +658,7 @@ public record ChatTemplate( return true; var builder = new StringBuilder(); - builder.AppendLine("{"); + builder.AppendLineLF("{"); for (var i = 0; i < this.ExampleConversation.Count; i++) { var block = this.ExampleConversation[i]; @@ -680,10 +680,10 @@ public record ChatTemplate( return false; } - builder.AppendLine(" {"); - builder.AppendLine($" [\"Role\"] = \"{block.Role}\","); - builder.AppendLine($" [\"Content\"] = {LuaTools.ToLuaStringLiteral(textContent.Text)},"); - builder.AppendLine(" },"); + builder.AppendLineLF(" {"); + builder.AppendLineLF($" [\"Role\"] = \"{block.Role}\","); + builder.AppendLineLF($" [\"Content\"] = {LuaTools.ToLuaStringLiteral(textContent.Text)},"); + builder.AppendLineLF(" },"); } builder.Append(" }"); @@ -693,7 +693,7 @@ public record ChatTemplate( private static string BuildOptionalStringLua(string fieldName, string? value) => value is null ? string.Empty - : $"{Environment.NewLine} [\"{fieldName}\"] = {LuaTools.ToLuaStringLiteral(value)},"; + : $"\n [\"{fieldName}\"] = {LuaTools.ToLuaStringLiteral(value)},"; /// /// An empty set is written out as an empty table rather than being left out: the two say @@ -706,21 +706,21 @@ public record ChatTemplate( return string.Empty; var builder = new StringBuilder(); - builder.AppendLine(); + builder.AppendLineLF(); if (this.ToolIds.Count == 0) { builder.Append(""" ["ToolIds"] = {},"""); return builder.ToString(); } - builder.AppendLine(""" ["ToolIds"] = {"""); + builder.AppendLineLF(""" ["ToolIds"] = {"""); // // A set has no order of its own, so exporting the same template twice would otherwise // produce two different files. Sorting keeps the plugin diffs readable: // foreach (var toolId in this.ToolIds.Order(StringComparer.Ordinal)) - builder.AppendLine($" {LuaTools.ToLuaStringLiteral(toolId)},"); + builder.AppendLineLF($" {LuaTools.ToLuaStringLiteral(toolId)},"); builder.Append(" },"); return builder.ToString(); @@ -732,22 +732,22 @@ public record ChatTemplate( return string.Empty; var builder = new StringBuilder(); - builder.AppendLine(); - builder.AppendLine(""" ["DataSourceOptions"] = {"""); - builder.AppendLine($""" ["DisableDataSources"] = {options.DisableDataSources.ToString().ToLowerInvariant()},"""); - builder.AppendLine($""" ["AutomaticDataSourceSelection"] = {options.AutomaticDataSourceSelection.ToString().ToLowerInvariant()},"""); - builder.AppendLine($""" ["AutomaticValidation"] = {options.AutomaticValidation.ToString().ToLowerInvariant()},"""); - builder.AppendLine($""" ["RetrievalMode"] = "{options.RetrievalMode}","""); + builder.AppendLineLF(); + builder.AppendLineLF(""" ["DataSourceOptions"] = {"""); + builder.AppendLineLF($""" ["DisableDataSources"] = {options.DisableDataSources.ToString().ToLowerInvariant()},"""); + builder.AppendLineLF($""" ["AutomaticDataSourceSelection"] = {options.AutomaticDataSourceSelection.ToString().ToLowerInvariant()},"""); + builder.AppendLineLF($""" ["AutomaticValidation"] = {options.AutomaticValidation.ToString().ToLowerInvariant()},"""); + builder.AppendLineLF($""" ["RetrievalMode"] = "{options.RetrievalMode}","""); if (options.PreselectedDataSourceIds.Count == 0) - builder.AppendLine(""" ["PreselectedDataSourceIds"] = {},"""); + builder.AppendLineLF(""" ["PreselectedDataSourceIds"] = {},"""); else { - builder.AppendLine(""" ["PreselectedDataSourceIds"] = {"""); + builder.AppendLineLF(""" ["PreselectedDataSourceIds"] = {"""); foreach (var dataSourceId in options.PreselectedDataSourceIds) - builder.AppendLine($" {LuaTools.ToLuaStringLiteral(dataSourceId)},"); + builder.AppendLineLF($" {LuaTools.ToLuaStringLiteral(dataSourceId)},"); - builder.AppendLine(" },"); + builder.AppendLineLF(" },"); } builder.Append(" },"); @@ -801,9 +801,9 @@ public record ChatTemplate( return "{}"; var builder = new StringBuilder(); - builder.AppendLine("{"); + builder.AppendLineLF("{"); foreach (var path in paths) - builder.AppendLine($" \"{LuaTools.EscapeLuaString(path)}\","); + builder.AppendLineLF($" \"{LuaTools.EscapeLuaString(path)}\","); builder.Append(" }"); return builder.ToString(); diff --git a/app/MindWork AI Studio/Settings/ExportedTokenizerPath.cs b/app/MindWork AI Studio/Settings/ExportedTokenizerPath.cs index 3c954f997..603e2fa6a 100644 --- a/app/MindWork AI Studio/Settings/ExportedTokenizerPath.cs +++ b/app/MindWork AI Studio/Settings/ExportedTokenizerPath.cs @@ -93,6 +93,6 @@ public static class ExportedTokenizerPath field, ]; - return string.Join($"{Environment.NewLine}{indentation}", lines); + return string.Join($"\n{indentation}", lines); } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ProviderCapabilityOverrides.cs b/app/MindWork AI Studio/Settings/ProviderCapabilityOverrides.cs index 2d1293f60..86882091a 100644 --- a/app/MindWork AI Studio/Settings/ProviderCapabilityOverrides.cs +++ b/app/MindWork AI Studio/Settings/ProviderCapabilityOverrides.cs @@ -346,14 +346,14 @@ public sealed record ProviderCapabilityOverrides return string.Empty; var builder = new StringBuilder(); - builder.AppendLine($@"{indentation}[""CapabilityOverrides""] = {{"); + builder.AppendLineLF($@"{indentation}[""CapabilityOverrides""] = {{"); foreach (var capability in SUPPORTED_CAPABILITIES) { var overrideValue = this.GetOverride(capability); if (overrideValue is null) continue; - builder.AppendLine($@"{indentation} [""{capability}""] = {overrideValue.Value.ToString().ToLowerInvariant()},"); + builder.AppendLineLF($@"{indentation} [""{capability}""] = {overrideValue.Value.ToString().ToLowerInvariant()},"); } foreach (var key in NUMERIC_KEYS) @@ -361,7 +361,7 @@ public sealed record ProviderCapabilityOverrides if (this.GetNumber(key) is not { } number) continue; - builder.AppendLine($@"{indentation} [""{key}""] = {number.ToString(CultureInfo.InvariantCulture)},"); + builder.AppendLineLF($@"{indentation} [""{key}""] = {number.ToString(CultureInfo.InvariantCulture)},"); } builder.Append($@"{indentation}}},"); diff --git a/app/MindWork AI Studio/Tools/ContentStreamSseHandler.cs b/app/MindWork AI Studio/Tools/ContentStreamSseHandler.cs index 60c3ac96e..9d8f74f3c 100644 --- a/app/MindWork AI Studio/Tools/ContentStreamSseHandler.cs +++ b/app/MindWork AI Studio/Tools/ContentStreamSseHandler.cs @@ -43,8 +43,8 @@ public static class ContentStreamSseHandler var spreadSheetResult = new StringBuilder(); if (rowNumber == 0) { - spreadSheetResult.AppendLine(); - spreadSheetResult.AppendLine($"# {sheetName}"); + spreadSheetResult.AppendLineLF(); + spreadSheetResult.AppendLineLF($"# {sheetName}"); } spreadSheetResult.Append(sseEvent.Content); diff --git a/app/MindWork AI Studio/Tools/CsvWriter.cs b/app/MindWork AI Studio/Tools/CsvWriter.cs index fe91f8295..3b4c6aaf5 100644 --- a/app/MindWork AI Studio/Tools/CsvWriter.cs +++ b/app/MindWork AI Studio/Tools/CsvWriter.cs @@ -8,6 +8,11 @@ namespace AIStudio.Tools; /// public static class CsvWriter { + /// + /// The end of every row: CR LF, as RFC 4180 asks for, on every system. + /// + public const string ROW_END = "\r\n"; + /// /// The separator a spreadsheet expects from a CSV file written for the given language. /// diff --git a/app/MindWork AI Studio/Tools/DataSourceReindexWarning.cs b/app/MindWork AI Studio/Tools/DataSourceReindexWarning.cs index 2353df966..19696ccad 100644 --- a/app/MindWork AI Studio/Tools/DataSourceReindexWarning.cs +++ b/app/MindWork AI Studio/Tools/DataSourceReindexWarning.cs @@ -122,18 +122,18 @@ public static class DataSourceReindexWarning private static async Task ConfirmNewLocationAsync(IDialogService dialogService, bool usesCloudEmbedding) { var body = new StringBuilder(); - body.AppendLine(TB("AI Studio keeps what it has already prepared for every file it finds unchanged at the new location, that is, of the same type and with the same size and modification time. In a folder, such a file also has to keep its name and the subfolder it is in.")); - body.AppendLine(); - body.AppendLine(TB("Every other file goes to your embedding provider once more, which takes a while with many files. Files missing at the new location are removed from the index, while your files on disk stay as they are.")); + body.AppendLineLF(TB("AI Studio keeps what it has already prepared for every file it finds unchanged at the new location, that is, of the same type and with the same size and modification time. In a folder, such a file also has to keep its name and the subfolder it is in.")); + body.AppendLineLF(); + body.AppendLineLF(TB("Every other file goes to your embedding provider once more, which takes a while with many files. Files missing at the new location are removed from the index, while your files on disk stay as they are.")); if (usesCloudEmbedding) { - body.AppendLine(); - body.AppendLine(TB("Your embedding provider runs in the cloud, so preparing files again costs money.")); + body.AppendLineLF(); + body.AppendLineLF(TB("Your embedding provider runs in the cloud, so preparing files again costs money.")); } - body.AppendLine(); - body.AppendLine(TB("Do you want to apply this change anyway?")); + body.AppendLineLF(); + body.AppendLineLF(TB("Do you want to apply this change anyway?")); var dialogParameters = new DialogParameters { @@ -173,11 +173,11 @@ public static class DataSourceReindexWarning var body = new StringBuilder(); // Counted rather than put into a plural form: the I18N has no mechanism for one. - body.AppendLine(string.Format(TB("These data sources are set up with this embedding provider ({0}):"), affected.Count.CompactCount())); - body.AppendLine(); - body.AppendLine(FormatDataSourceNames(affected)); - body.AppendLine(); - body.AppendLine(TB("They keep answering keyword searches, but searching them by meaning stops working, and no further documents can be prepared for them. The ones which are already prepared stay tied to this provider as well, so you cannot simply move them to another one.")); + body.AppendLineLF(string.Format(TB("These data sources are set up with this embedding provider ({0}):"), affected.Count.CompactCount())); + body.AppendLineLF(); + body.AppendLineLF(FormatDataSourceNames(affected)); + body.AppendLineLF(); + body.AppendLineLF(TB("They keep answering keyword searches, but searching them by meaning stops working, and no further documents can be prepared for them. The ones which are already prepared stay tied to this provider as well, so you cannot simply move them to another one.")); return body.ToString(); } @@ -211,7 +211,7 @@ public static class DataSourceReindexWarning if (names.Count > MAX_NAMED_DATA_SOURCES) lines.Add($"- {string.Format(TB("and {0} more."), (names.Count - MAX_NAMED_DATA_SOURCES).CompactCount())}"); - return string.Join(Environment.NewLine, lines); + return string.Join('\n', lines); } private static async Task ConfirmAsync(IDialogService dialogService, IReadOnlyList affected, bool usesCloudEmbedding) @@ -222,20 +222,20 @@ public static class DataSourceReindexWarning var body = new StringBuilder(); // Counted rather than put into a plural form: the I18N has no mechanism for one. - body.AppendLine(string.Format(TB("This change makes the prepared documents of the following data sources unusable ({0}):"), affected.Count.CompactCount())); - body.AppendLine(); - body.AppendLine(FormatDataSourceNames(affected)); - body.AppendLine(); - body.AppendLine(TB("Everything prepared for them is thrown away, and every one of their documents goes to your embedding provider once more. With a large data source, this takes a while.")); + body.AppendLineLF(string.Format(TB("This change makes the prepared documents of the following data sources unusable ({0}):"), affected.Count.CompactCount())); + body.AppendLineLF(); + body.AppendLineLF(FormatDataSourceNames(affected)); + body.AppendLineLF(); + body.AppendLineLF(TB("Everything prepared for them is thrown away, and every one of their documents goes to your embedding provider once more. With a large data source, this takes a while.")); if (usesCloudEmbedding) { - body.AppendLine(); - body.AppendLine(TB("Your embedding provider runs in the cloud, so preparing everything again costs money.")); + body.AppendLineLF(); + body.AppendLineLF(TB("Your embedding provider runs in the cloud, so preparing everything again costs money.")); } - body.AppendLine(); - body.AppendLine(TB("Do you want to apply this change anyway?")); + body.AppendLineLF(); + body.AppendLineLF(TB("Do you want to apply this change anyway?")); var dialogParameters = new DialogParameters { diff --git a/app/MindWork AI Studio/Tools/DocumentManager.cs b/app/MindWork AI Studio/Tools/DocumentManager.cs index d4b911351..dc2ed08b9 100644 --- a/app/MindWork AI Studio/Tools/DocumentManager.cs +++ b/app/MindWork AI Studio/Tools/DocumentManager.cs @@ -32,8 +32,8 @@ public sealed class DocumentManager // headings, leaving the AI unable to tell the structure of the document apart from our // boundaries. The presentation reader marks its slides the same way. // - this.currentPageContent.AppendLine(metadata.Document?.PageIsExact is true ? $"" : $""); - this.currentPageContent.AppendLine(); + this.currentPageContent.AppendLineLF(metadata.Document?.PageIsExact is true ? $"" : $""); + this.currentPageContent.AppendLineLF(); this.currentPageContent.Append(content); // @@ -55,8 +55,8 @@ public sealed class DocumentManager var markdownImage = ContentStreamSseHandler.BuildImageMarkdown(image.Id, image.MediaType); if (markdownImage is not null) { - this.currentPageContent.AppendLine(); - this.currentPageContent.AppendLine(markdownImage); + this.currentPageContent.AppendLineLF(); + this.currentPageContent.AppendLineLF(markdownImage); // // The runtime counted the text of this page, not the image we just embedded into it. diff --git a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs index 26a18a372..0834a6ca3 100644 --- a/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs +++ b/app/MindWork AI Studio/Tools/FileExportFormatExtensions.cs @@ -202,12 +202,12 @@ public static class FileExportFormatExtensions switch (format) { case FileExportFormat.HTML or FileExportFormat.MARKDOWN: - var commentText = string.Join(Environment.NewLine, lines).Replace("-->", "-- >").Replace("--!>", "--! >"); - comment = $""; + var commentText = string.Join('\n', lines).Replace("-->", "-- >").Replace("--!>", "--! >"); + comment = $""; return true; case FileExportFormat.LATEX: - comment = string.Join(Environment.NewLine, lines.Select(line => line.Length is 0 ? "%" : $"% {line}")); + comment = string.Join('\n', lines.Select(line => line.Length is 0 ? "%" : $"% {line}")); return true; default: diff --git a/app/MindWork AI Studio/Tools/Markdown.cs b/app/MindWork AI Studio/Tools/Markdown.cs index cc7ef6234..e11cc3097 100644 --- a/app/MindWork AI Studio/Tools/Markdown.cs +++ b/app/MindWork AI Studio/Tools/Markdown.cs @@ -99,7 +99,7 @@ public static class Markdown if (document.Descendants().LastOrDefault() is not { ClosingFencedCharCount: 0 } openFence) return markdownText; - return $"{markdownText}{Environment.NewLine}{new string(openFence.FencedChar, openFence.OpeningFencedCharCount)}"; + return $"{markdownText}\n{new string(openFence.FencedChar, openFence.OpeningFencedCharCount)}"; } /// Puts a block of Markdown below a Markdown text, such as its sources or a notice. @@ -118,7 +118,7 @@ public static class Markdown // The blank line is not cosmetic: it ends a paragraph, a list, a table, or a block quote, so // that the block stands on its own instead of being pulled into the last block of the text. // - return $"{CloseOpenCodeFence(markdownText)}{Environment.NewLine}{Environment.NewLine}{block}"; + return $"{CloseOpenCodeFence(markdownText)}\n\n{block}"; } public static string RemoveSharedIndentation(string value) diff --git a/app/MindWork AI Studio/Tools/PlainFileExport.cs b/app/MindWork AI Studio/Tools/PlainFileExport.cs index ad3c306ad..524ae9046 100644 --- a/app/MindWork AI Studio/Tools/PlainFileExport.cs +++ b/app/MindWork AI Studio/Tools/PlainFileExport.cs @@ -113,7 +113,7 @@ public static class PlainFileExport /// Turns a formula into a file of LaTeX, between \[ and \] as a formula of its own, so that it can be /// pasted into a document as it is. /// - private static FoundFile ReadFormula(string formula) => new(string.Empty, FileExportFormat.LATEX, SourceCodeLanguage.NONE, $@"\[{Environment.NewLine}{formula}{Environment.NewLine}\]"); + private static FoundFile ReadFormula(string formula) => new(string.Empty, FileExportFormat.LATEX, SourceCodeLanguage.NONE, $"\\[\n{formula}\n\\]"); /// /// Turns a Markdown table into a file. @@ -130,7 +130,7 @@ public static class PlainFileExport var text = new StringBuilder(); foreach (var fields in rows) - text.AppendLine(CsvWriter.ToRow(separator, fields)); + text.Append(CsvWriter.ToRow(separator, fields)).Append(CsvWriter.ROW_END); return new(rows[0].FirstOrDefault() ?? string.Empty, FileExportFormat.CSV, SourceCodeLanguage.NONE, text.ToString()); } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdown.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdown.cs index a2ec02705..2eb4a1e1a 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdown.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdown.cs @@ -113,7 +113,7 @@ internal sealed class AssistantDropdown : StatefulAssistantComponentBase public override string UserPromptFallback(AssistantState state) { if (this.IsMultiselect && state.MultiSelect.TryGetValue(this.Name, out var selections)) - return this.BuildAuditPromptBlock(string.Join(Environment.NewLine, selections.OrderBy(static value => value, StringComparer.Ordinal))); + return this.BuildAuditPromptBlock(string.Join('\n', selections.OrderBy(static value => value, StringComparer.Ordinal))); state.SingleSelect.TryGetValue(this.Name, out var userInput); return this.BuildAuditPromptBlock(userInput); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantFileAttachments.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantFileAttachments.cs index 58b484990..cf668e2b9 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantFileAttachments.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantFileAttachments.cs @@ -57,7 +57,7 @@ internal sealed class AssistantFileAttachment : StatefulAssistantComponentBase var builder = new StringBuilder(); foreach (var attachment in fileState.DocumentPaths.OrderBy(static attachment => attachment.FilePath, StringComparer.Ordinal)) - builder.AppendLine(attachment.FilePath); + builder.AppendLineLF(attachment.FilePath); return this.BuildAuditPromptBlock(builder.ToString()); } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantLuaConversion.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantLuaConversion.cs index 285a960a2..a5394cb14 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantLuaConversion.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantLuaConversion.cs @@ -282,14 +282,14 @@ internal static class AssistantLuaConversion var indent = new string(' ', depth * 2); var childIndent = new string(' ', (depth + 1) * 2); var builder = new System.Text.StringBuilder(); - builder.AppendLine("{"); + builder.AppendLineLF("{"); foreach (var entry in table) { builder.Append(childIndent); builder.Append(FormatLuaValue(entry.Key)); builder.Append(" = "); - builder.AppendLine(FormatLuaValue(entry.Value, depth + 1)); + builder.AppendLineLF(FormatLuaValue(entry.Value, depth + 1)); } builder.Append(indent); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/StatefulAssistantComponentBase.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/StatefulAssistantComponentBase.cs index b9031ef7b..4f76dfbc0 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/StatefulAssistantComponentBase.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/StatefulAssistantComponentBase.cs @@ -18,13 +18,13 @@ public abstract class StatefulAssistantComponentBase : NamedAssistantComponentBa var builder = new StringBuilder(); var fieldName = this.Type.ToString().ToLowerInvariant(); - builder.AppendLine($"[{fieldName}]"); - builder.Append("name: ").AppendLine(this.Name); - builder.AppendLine("context:"); - builder.AppendLine(!string.IsNullOrEmpty(this.UserPrompt) ? this.UserPrompt : ""); - builder.AppendLine("value:"); - builder.AppendLine(!string.IsNullOrEmpty(value) ? value : ""); - builder.Append($"[/{fieldName}]").AppendLine().AppendLine(); + builder.AppendLineLF($"[{fieldName}]"); + builder.Append("name: ").AppendLineLF(this.Name); + builder.AppendLineLF("context:"); + builder.AppendLineLF(!string.IsNullOrEmpty(this.UserPrompt) ? this.UserPrompt : ""); + builder.AppendLineLF("value:"); + builder.AppendLineLF(!string.IsNullOrEmpty(value) ? value : ""); + builder.Append($"[/{fieldName}]").AppendLineLF().AppendLineLF(); return builder.ToString(); } } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DirectChatLauncherLuaWriter.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DirectChatLauncherLuaWriter.cs index 9660d9fd3..14cccb895 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DirectChatLauncherLuaWriter.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DirectChatLauncherLuaWriter.cs @@ -95,71 +95,71 @@ public static class DirectChatLauncherLuaWriter { var builder = new StringBuilder(); - builder.AppendLine("--[["); - builder.AppendLine(" This direct chat launcher is maintained by AI Studio: its settings dialog rewrites this"); - builder.AppendLine(" file as a whole. Editing it by hand works, but the next change made through the dialog"); - builder.AppendLine(" replaces everything below, including comments and formatting."); - builder.AppendLine("]]"); - builder.AppendLine(); + builder.AppendLineLF("--[["); + builder.AppendLineLF(" This direct chat launcher is maintained by AI Studio: its settings dialog rewrites this"); + builder.AppendLineLF(" file as a whole. Editing it by hand works, but the next change made through the dialog"); + builder.AppendLineLF(" replaces everything below, including comments and formatting."); + builder.AppendLineLF("]]"); + builder.AppendLineLF(); - builder.AppendLine("-- The ID for this plugin:"); - builder.AppendLine($"ID = \"{plugin.Id}\""); - builder.AppendLine(); + builder.AppendLineLF("-- The ID for this plugin:"); + builder.AppendLineLF($"ID = \"{plugin.Id}\""); + builder.AppendLineLF(); - builder.AppendLine("-- The name of the plugin:"); - builder.AppendLine($"NAME = \"{Escape(definition.PluginName)}\""); - builder.AppendLine(); + builder.AppendLineLF("-- The name of the plugin:"); + builder.AppendLineLF($"NAME = \"{Escape(definition.PluginName)}\""); + builder.AppendLineLF(); - builder.AppendLine("-- The description of the plugin:"); - builder.AppendLine($"DESCRIPTION = \"{Escape(definition.Description)}\""); - builder.AppendLine(); + builder.AppendLineLF("-- The description of the plugin:"); + builder.AppendLineLF($"DESCRIPTION = \"{Escape(definition.Description)}\""); + builder.AppendLineLF(); - builder.AppendLine("-- The version of the plugin:"); - builder.AppendLine($"VERSION = \"{plugin.Version}\""); - builder.AppendLine(); + builder.AppendLineLF("-- The version of the plugin:"); + builder.AppendLineLF($"VERSION = \"{plugin.Version}\""); + builder.AppendLineLF(); if (plugin.LastChanged is { } lastChanged) { - builder.AppendLine("-- When this plugin was last changed, in UTC, formatted as YYYY-MM-DDTHH:MM:SSZ. Optional."); - builder.AppendLine($"LAST_CHANGED = \"{PluginBase.FormatLastChanged(lastChanged)}\""); - builder.AppendLine(); + builder.AppendLineLF("-- When this plugin was last changed, in UTC, formatted as YYYY-MM-DDTHH:MM:SSZ. Optional."); + builder.AppendLineLF($"LAST_CHANGED = \"{PluginBase.FormatLastChanged(lastChanged)}\""); + builder.AppendLineLF(); } - builder.AppendLine("-- The type of the plugin:"); - builder.AppendLine($"TYPE = \"{nameof(PluginType.ASSISTANT)}\""); - builder.AppendLine(); + builder.AppendLineLF("-- The type of the plugin:"); + builder.AppendLineLF($"TYPE = \"{nameof(PluginType.ASSISTANT)}\""); + builder.AppendLineLF(); - builder.AppendLine("-- The authors of the plugin:"); - builder.AppendLine($"AUTHORS = {WriteStringList(plugin.Authors, FALLBACK_AUTHOR)}"); - builder.AppendLine(); + builder.AppendLineLF("-- The authors of the plugin:"); + builder.AppendLineLF($"AUTHORS = {WriteStringList(plugin.Authors, FALLBACK_AUTHOR)}"); + builder.AppendLineLF(); - builder.AppendLine("-- The support contact for the plugin:"); - builder.AppendLine($"SUPPORT_CONTACT = \"{Escape(ValueOrFallback(plugin.SupportContact, FALLBACK_SUPPORT_CONTACT))}\""); - builder.AppendLine(); + builder.AppendLineLF("-- The support contact for the plugin:"); + builder.AppendLineLF($"SUPPORT_CONTACT = \"{Escape(ValueOrFallback(plugin.SupportContact, FALLBACK_SUPPORT_CONTACT))}\""); + builder.AppendLineLF(); - builder.AppendLine("-- The source URL for the plugin:"); - builder.AppendLine($"SOURCE_URL = \"{Escape(ValueOrFallback(plugin.SourceURL, FALLBACK_SOURCE_URL))}\""); - builder.AppendLine(); + builder.AppendLineLF("-- The source URL for the plugin:"); + builder.AppendLineLF($"SOURCE_URL = \"{Escape(ValueOrFallback(plugin.SourceURL, FALLBACK_SOURCE_URL))}\""); + builder.AppendLineLF(); - builder.AppendLine("-- The categories for the plugin:"); - builder.AppendLine($"CATEGORIES = {WriteEnumList(plugin.Categories, FALLBACK_CATEGORY)}"); - builder.AppendLine(); + builder.AppendLineLF("-- The categories for the plugin:"); + builder.AppendLineLF($"CATEGORIES = {WriteEnumList(plugin.Categories, FALLBACK_CATEGORY)}"); + builder.AppendLineLF(); - builder.AppendLine("-- The target groups for the plugin:"); - builder.AppendLine($"TARGET_GROUPS = {WriteEnumList(plugin.TargetGroups, FALLBACK_TARGET_GROUP)}"); - builder.AppendLine(); + builder.AppendLineLF("-- The target groups for the plugin:"); + builder.AppendLineLF($"TARGET_GROUPS = {WriteEnumList(plugin.TargetGroups, FALLBACK_TARGET_GROUP)}"); + builder.AppendLineLF(); - builder.AppendLine("-- The flag for whether the plugin is maintained:"); - builder.AppendLine($"IS_MAINTAINED = {WriteBoolean(plugin.IsMaintained)}"); - builder.AppendLine(); + builder.AppendLineLF("-- The flag for whether the plugin is maintained:"); + builder.AppendLineLF($"IS_MAINTAINED = {WriteBoolean(plugin.IsMaintained)}"); + builder.AppendLineLF(); - builder.AppendLine("-- When the plugin is deprecated, this message will be shown to users:"); - builder.AppendLine($"DEPRECATION_MESSAGE = \"{Escape(plugin.DeprecationMessage)}\""); - builder.AppendLine(); + builder.AppendLineLF("-- When the plugin is deprecated, this message will be shown to users:"); + builder.AppendLineLF($"DEPRECATION_MESSAGE = \"{Escape(plugin.DeprecationMessage)}\""); + builder.AppendLineLF(); - builder.AppendLine("-- Enterprise-managed assistants cannot be revised with AI. Keep false for locally managed plugins:"); - builder.AppendLine("DEPLOYED_USING_CONFIG_SERVER = false"); - builder.AppendLine(); + builder.AppendLineLF("-- Enterprise-managed assistants cannot be revised with AI. Keep false for locally managed plugins:"); + builder.AppendLineLF("DEPLOYED_USING_CONFIG_SERVER = false"); + builder.AppendLineLF(); // // This metadata marks assistants the Builder created and must not appear on manually @@ -167,26 +167,26 @@ public static class DirectChatLauncherLuaWriter // if (plugin.IsAssistantBuilderGenerated) { - builder.AppendLine("-- This assistant was created by the AI Studio Assistant Builder:"); - builder.AppendLine("AI_STUDIO_ASSISTANT_BUILDER = {Generated = true, SchemaVersion = 1}"); - builder.AppendLine(); + builder.AppendLineLF("-- This assistant was created by the AI Studio Assistant Builder:"); + builder.AppendLineLF("AI_STUDIO_ASSISTANT_BUILDER = {Generated = true, SchemaVersion = 1}"); + builder.AppendLineLF(); } - builder.AppendLine("-- The tile opens a chat directly, hence it needs no system prompt, no submit text, and no UI:"); - builder.AppendLine("ASSISTANT = {"); - builder.AppendLine($" [\"Title\"] = \"{Escape(definition.Title)}\","); - builder.AppendLine($" [\"Description\"] = \"{Escape(definition.Description)}\","); + builder.AppendLineLF("-- The tile opens a chat directly, hence it needs no system prompt, no submit text, and no UI:"); + builder.AppendLineLF("ASSISTANT = {"); + builder.AppendLineLF($" [\"Title\"] = \"{Escape(definition.Title)}\","); + builder.AppendLineLF($" [\"Description\"] = \"{Escape(definition.Description)}\","); // // The behavior follows from the workspace rather than being tracked next to it. A launcher // without one has no name to write, and the plugin loader rejects a WorkspaceName there, so // the field is left out the same way the optional IDs below are: // if (definition.Launch.OpensTemporaryChat) - builder.AppendLine($" [\"LaunchBehavior\"] = \"{nameof(AssistantPluginLaunchBehavior.OPEN_TEMPORARY_CHAT)}\","); + builder.AppendLineLF($" [\"LaunchBehavior\"] = \"{nameof(AssistantPluginLaunchBehavior.OPEN_TEMPORARY_CHAT)}\","); else { - builder.AppendLine($" [\"LaunchBehavior\"] = \"{nameof(AssistantPluginLaunchBehavior.OPEN_WORKSPACE_CHAT_BY_NAME)}\","); - builder.AppendLine($" [\"WorkspaceName\"] = \"{Escape(definition.Launch.WorkspaceName.Trim())}\","); + builder.AppendLineLF($" [\"LaunchBehavior\"] = \"{nameof(AssistantPluginLaunchBehavior.OPEN_WORKSPACE_CHAT_BY_NAME)}\","); + builder.AppendLineLF($" [\"WorkspaceName\"] = \"{Escape(definition.Launch.WorkspaceName.Trim())}\","); } // @@ -194,30 +194,30 @@ public static class DirectChatLauncherLuaWriter // profile or no chat template. An empty provider GUID has no such meaning and is invalid: // if (definition.Launch.ProviderId is { } providerId && providerId != Guid.Empty) - builder.AppendLine($" [\"ProviderId\"] = \"{providerId}\","); + builder.AppendLineLF($" [\"ProviderId\"] = \"{providerId}\","); if (definition.Launch.ProfileId is { } profileId) - builder.AppendLine($" [\"ProfileId\"] = \"{profileId}\","); + builder.AppendLineLF($" [\"ProfileId\"] = \"{profileId}\","); if (definition.Launch.ChatTemplateId is { } chatTemplateId) - builder.AppendLine($" [\"ChatTemplateId\"] = \"{chatTemplateId}\","); + builder.AppendLineLF($" [\"ChatTemplateId\"] = \"{chatTemplateId}\","); if (definition.Launch.DataSourceIds is { Count: > 0 } dataSourceIds) { - builder.AppendLine(" [\"DataSourceIds\"] = {"); + builder.AppendLineLF(" [\"DataSourceIds\"] = {"); foreach (var dataSourceId in dataSourceIds) - builder.AppendLine($" \"{dataSourceId}\","); + builder.AppendLineLF($" \"{dataSourceId}\","); - builder.AppendLine(" },"); + builder.AppendLineLF(" },"); } if (definition.Launch.ToolIds is { Count: > 0 } toolIds) { - builder.AppendLine(" [\"ToolIds\"] = {"); + builder.AppendLineLF(" [\"ToolIds\"] = {"); foreach (var toolId in toolIds) - builder.AppendLine($" {LuaTools.ToLuaStringLiteral(toolId)},"); + builder.AppendLineLF($" {LuaTools.ToLuaStringLiteral(toolId)},"); - builder.AppendLine(" },"); + builder.AppendLineLF(" },"); } builder.Append('}'); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs index 4b99458b4..b7728e5e6 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs @@ -574,7 +574,7 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType private static string BuildSecureSystemPrompt(string pluginSystemPrompt) { - var separator = $"{Environment.NewLine}{Environment.NewLine}"; + var separator = "\n\n"; return string.IsNullOrWhiteSpace(pluginSystemPrompt) ? $"{SECURITY_SYSTEM_PROMPT_PREAMBLE}{separator}{SECURITY_SYSTEM_PROMPT_POSTAMBLE}" : $"{SECURITY_SYSTEM_PROMPT_PREAMBLE}{separator}{pluginSystemPrompt.Trim()}{separator}{SECURITY_SYSTEM_PROMPT_POSTAMBLE}"; } @@ -939,7 +939,7 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType builder.Append(string.IsNullOrWhiteSpace(stateful.UserPrompt) ? "empty" : "set"); } - builder.AppendLine(); + builder.AppendLineLF(); if (component.Children.Count > 0) AppendComponentSummary(builder, component.Children, depth + 1); diff --git a/app/MindWork AI Studio/Tools/RAG/AugmentationProcesses/AugmentationOne.cs b/app/MindWork AI Studio/Tools/RAG/AugmentationProcesses/AugmentationOne.cs index bec1a4a69..cf4c842bf 100644 --- a/app/MindWork AI Studio/Tools/RAG/AugmentationProcesses/AugmentationOne.cs +++ b/app/MindWork AI Studio/Tools/RAG/AugmentationProcesses/AugmentationOne.cs @@ -138,8 +138,8 @@ public sealed class AugmentationOne : IAugmentationProcess var sb = new StringBuilder(); if (selection.Contexts.Count > 0) { - sb.AppendLine("The following useful information will help you in processing the user prompt:"); - sb.AppendLine(); + sb.AppendLineLF("The following useful information will help you in processing the user prompt:"); + sb.AppendLineLF(); // Let's convert the retrieval contexts to Markdown: for (var index = 0; index < selection.Contexts.Count; index++) @@ -147,7 +147,7 @@ public sealed class AugmentationOne : IAugmentationProcess } if (selection.LeftOutCount > 0) - sb.AppendLine(selection.LeftOutCount is 1 + sb.AppendLineLF(selection.LeftOutCount is 1 ? "Of the passages which the search found in the data sources, 1 did not fit into this request and was left out. An answer based on the data sources may therefore be incomplete." : $"Of the passages which the search found in the data sources, {selection.LeftOutCount} did not fit into this request and were left out. An answer based on the data sources may therefore be incomplete."); diff --git a/app/MindWork AI Studio/Tools/RAG/PreparedRetrievalContext.cs b/app/MindWork AI Studio/Tools/RAG/PreparedRetrievalContext.cs index 9969702d6..756c69bf8 100644 --- a/app/MindWork AI Studio/Tools/RAG/PreparedRetrievalContext.cs +++ b/app/MindWork AI Studio/Tools/RAG/PreparedRetrievalContext.cs @@ -29,20 +29,20 @@ public sealed record PreparedRetrievalContext(IRetrievalContext Context, string switch (index) { case > 0 when numTotalRetrievalContexts is null: - sb.AppendLine($"# Retrieval context {index}"); + sb.AppendLineLF($"# Retrieval context {index}"); break; case > 0 when numTotalRetrievalContexts > 0: - sb.AppendLine($"# Retrieval context {index} of {numTotalRetrievalContexts}"); + sb.AppendLineLF($"# Retrieval context {index} of {numTotalRetrievalContexts}"); break; default: - sb.AppendLine("# Retrieval context"); + sb.AppendLineLF("# Retrieval context"); break; } sb.Append(this.Body); - sb.AppendLine(); + sb.AppendLineLF(); return sb.ToString(); } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/RAG/RAGProcesses/AISrcSelWithRetCtxVal.cs b/app/MindWork AI Studio/Tools/RAG/RAGProcesses/AISrcSelWithRetCtxVal.cs index 59f55c581..82503db9b 100644 --- a/app/MindWork AI Studio/Tools/RAG/RAGProcesses/AISrcSelWithRetCtxVal.cs +++ b/app/MindWork AI Studio/Tools/RAG/RAGProcesses/AISrcSelWithRetCtxVal.cs @@ -330,7 +330,7 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess var builder = new StringBuilder(augmentedData); if (builder.Length > 0) - builder.AppendLine(); + builder.AppendLineLF(); UnreachableDataSources.AppendTo(builder, unreachableDataSources); return builder.ToString(); diff --git a/app/MindWork AI Studio/Tools/RAG/RetrievalContextExtensions.cs b/app/MindWork AI Studio/Tools/RAG/RetrievalContextExtensions.cs index 9a4b36740..ded35c3aa 100644 --- a/app/MindWork AI Studio/Tools/RAG/RetrievalContextExtensions.cs +++ b/app/MindWork AI Studio/Tools/RAG/RetrievalContextExtensions.cs @@ -23,23 +23,23 @@ public static class RetrievalContextExtensions /// The context to describe. internal static void AppendContextDescription(StringBuilder contextBuilder, IRetrievalContext retrievalContext) { - contextBuilder.AppendLine($"Data source name: {retrievalContext.DataSourceName}"); - contextBuilder.AppendLine($"Content category: {retrievalContext.Category}"); + contextBuilder.AppendLineLF($"Data source name: {retrievalContext.DataSourceName}"); + contextBuilder.AppendLineLF($"Content category: {retrievalContext.Category}"); if(!string.IsNullOrWhiteSpace(retrievalContext.SourceCategory)) - contextBuilder.AppendLine($"Source category: {retrievalContext.SourceCategory.ReplaceLineEndings(" ").Trim()}"); + contextBuilder.AppendLineLF($"Source category: {retrievalContext.SourceCategory.ReplaceLineEndings(" ").Trim()}"); - contextBuilder.AppendLine($"Content type: {retrievalContext.Type}"); - contextBuilder.AppendLine($"Content path: {retrievalContext.Path}"); + contextBuilder.AppendLineLF($"Content type: {retrievalContext.Type}"); + contextBuilder.AppendLineLF($"Content path: {retrievalContext.Path}"); if(retrievalContext is RetrievalTextContext textContext && DescribeLocation(textContext.PageNumber, textContext.EndPageNumber, textContext.SheetName, textContext.StartRow, textContext.EndRow, textContext.StartLine, textContext.EndLine) is { } location) - contextBuilder.AppendLine($"Content location: {location}"); + contextBuilder.AppendLineLF($"Content location: {location}"); if(retrievalContext.Links.Count is 0) return; - contextBuilder.AppendLine("Additional links:"); + contextBuilder.AppendLineLF("Additional links:"); foreach(var link in retrievalContext.Links) - contextBuilder.AppendLine($"- {link}"); + contextBuilder.AppendLineLF($"- {link}"); } /// @@ -86,8 +86,8 @@ public static class RetrievalContextExtensions /// The builder to write into, after the description of the context. internal static void AppendImageMatch(StringBuilder contextBuilder) { - contextBuilder.AppendLine(); - contextBuilder.AppendLine("Matched image content: an image matched here, but it is not shown to you. Do not guess what it shows. When it matters for the answer, tell the user that an image matched, and where to find it, using the path and the links above."); + contextBuilder.AppendLineLF(); + contextBuilder.AppendLineLF("Matched image content: an image matched here, but it is not shown to you. Do not guess what it shows. When it matters for the answer, tell the user that an image matched, and where to find it, using the path and the links above."); } /// The contexts to prepare. @@ -295,31 +295,31 @@ public static class RetrievalContextExtensions /// The surrounding content, filtered. private static void AppendTextMatch(StringBuilder contextBuilder, RetrievalTextContext textContext, string matchedText, IReadOnlyList surroundingContent) { - contextBuilder.AppendLine(); - contextBuilder.AppendLine(textContext.IsMatchedTextShortened ? "Matched text content, shortened to fit the size limit:" : "Matched text content:"); - contextBuilder.AppendLine("````"); - contextBuilder.AppendLine(matchedText); - contextBuilder.AppendLine("````"); + contextBuilder.AppendLineLF(); + contextBuilder.AppendLineLF(textContext.IsMatchedTextShortened ? "Matched text content, shortened to fit the size limit:" : "Matched text content:"); + contextBuilder.AppendLineLF("````"); + contextBuilder.AppendLineLF(matchedText); + contextBuilder.AppendLineLF("````"); if (surroundingContent.Count is 0) { if (textContext.IsSurroundingContentShortened) { - contextBuilder.AppendLine(); - contextBuilder.AppendLine("Surrounding text content: left out to fit the size limit."); + contextBuilder.AppendLineLF(); + contextBuilder.AppendLineLF("Surrounding text content: left out to fit the size limit."); } return; } - contextBuilder.AppendLine(); - contextBuilder.AppendLine(textContext.IsSurroundingContentShortened ? "Surrounding text content, shortened to fit the size limit:" : "Surrounding text content:"); + contextBuilder.AppendLineLF(); + contextBuilder.AppendLineLF(textContext.IsSurroundingContentShortened ? "Surrounding text content, shortened to fit the size limit:" : "Surrounding text content:"); foreach (var surrounding in surroundingContent) { - contextBuilder.AppendLine(); - contextBuilder.AppendLine("````"); - contextBuilder.AppendLine(surrounding); - contextBuilder.AppendLine("````"); + contextBuilder.AppendLineLF(); + contextBuilder.AppendLineLF("````"); + contextBuilder.AppendLineLF(surrounding); + contextBuilder.AppendLineLF("````"); } } diff --git a/app/MindWork AI Studio/Tools/RAG/UnreachableDataSources.cs b/app/MindWork AI Studio/Tools/RAG/UnreachableDataSources.cs index 8c08c0af0..b8c4d0dbf 100644 --- a/app/MindWork AI Studio/Tools/RAG/UnreachableDataSources.cs +++ b/app/MindWork AI Studio/Tools/RAG/UnreachableDataSources.cs @@ -26,8 +26,8 @@ public static class UnreachableDataSources /// The data sources whose server could not be reached, in the order to list them. public static void AppendTo(StringBuilder builder, IReadOnlyList dataSources) { - builder.AppendLine("These external data sources of this chat could not be reached or signed in to right now, so they cannot be searched. Their content is missing rather than empty; tell the user so when a question concerns them:"); + builder.AppendLineLF("These external data sources of this chat could not be reached or signed in to right now, so they cannot be searched. Their content is missing rather than empty; tell the user so when a question concerns them:"); foreach (var dataSource in dataSources) - builder.AppendLine($"- name='{dataSource.Name}'"); + builder.AppendLineLF($"- name='{dataSource.Name}'"); } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/AssistantPluginGenerationService.cs b/app/MindWork AI Studio/Tools/Services/AssistantPluginGenerationService.cs index 5e5fb78d3..1be111dd2 100644 --- a/app/MindWork AI Studio/Tools/Services/AssistantPluginGenerationService.cs +++ b/app/MindWork AI Studio/Tools/Services/AssistantPluginGenerationService.cs @@ -261,24 +261,24 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry, continue; } - builder.AppendLine($"# {contextFile.Title}"); - builder.AppendLine($"Source: {contextFile.RelativePath}"); - builder.AppendLine(""); - builder.AppendLine(content.Trim()); - builder.AppendLine(""); - builder.AppendLine(); + builder.AppendLineLF($"# {contextFile.Title}"); + builder.AppendLineLF($"Source: {contextFile.RelativePath}"); + builder.AppendLineLF(""); + builder.AppendLineLF(content.Trim()); + builder.AppendLineLF(""); + builder.AppendLineLF(); } // // Unlike the files above, this list is not the same on two installations. It is the only // place the model learns which tool IDs exist, so an assistant cannot name a tool without it: // - builder.AppendLine("# Available tools"); - builder.AppendLine("Source: the tools installed in this AI Studio"); - builder.AppendLine(""); - builder.AppendLine(await this.FormatAvailableToolsAsync()); - builder.AppendLine(""); - builder.AppendLine(); + builder.AppendLineLF("# Available tools"); + builder.AppendLineLF("Source: the tools installed in this AI Studio"); + builder.AppendLineLF(""); + builder.AppendLineLF(await this.FormatAvailableToolsAsync()); + builder.AppendLineLF(""); + builder.AppendLineLF(); return builder.ToString().Trim(); } @@ -302,7 +302,7 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry, var builder = new StringBuilder(); foreach (var tool in activeTools) - builder.AppendLine($"- {tool.Id}: {tool.DescriptionForLLM}"); + builder.AppendLineLF($"- {tool.Id}: {tool.DescriptionForLLM}"); return builder.ToString().TrimEnd(); } @@ -729,11 +729,11 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry, var builder = new StringBuilder(); foreach (var (relativePath, content) in luaFiles) { - builder.AppendLine($"# {relativePath}"); - builder.AppendLine("```lua"); - builder.AppendLine(content.Trim()); - builder.AppendLine("```"); - builder.AppendLine(); + builder.AppendLineLF($"# {relativePath}"); + builder.AppendLineLF("```lua"); + builder.AppendLineLF(content.Trim()); + builder.AppendLineLF("```"); + builder.AppendLineLF(); } return builder.ToString().Trim(); diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs b/app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs index de06dddcb..33779439f 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs @@ -304,7 +304,7 @@ public sealed partial class RustService if (content.Length >= maxCharacters) return string.IsNullOrWhiteSpace(piece); - content.AppendLine(piece); + content.AppendLineLF(piece); return true; } diff --git a/app/MindWork AI Studio/Tools/SlideManager.cs b/app/MindWork AI Studio/Tools/SlideManager.cs index b3c648412..02cd363e8 100644 --- a/app/MindWork AI Studio/Tools/SlideManager.cs +++ b/app/MindWork AI Studio/Tools/SlideManager.cs @@ -28,12 +28,12 @@ public sealed class SlideManager // var contentBuilder = new StringBuilder(); - contentBuilder.AppendLine(); - contentBuilder.AppendLine($"# Slide {slideNumber}"); + contentBuilder.AppendLineLF(); + contentBuilder.AppendLineLF($"# Slide {slideNumber}"); // Add any text content to the slide? if(!string.IsNullOrWhiteSpace(content)) - contentBuilder.AppendLine(content); + contentBuilder.AppendLineLF(content); // // Add the text content to the slide: @@ -78,7 +78,7 @@ public sealed class SlideManager if (!string.IsNullOrWhiteSpace(content)) { var textContent = slide.Content.OfType().First(); - textContent.Text.AppendLine(content); + textContent.Text.AppendLineLF(content); slide.TokenCount = ContentStreamPendingContent.AddTokenCounts(slide.TokenCount, tokenCount); } @@ -112,14 +112,14 @@ public sealed class SlideManager foreach (var text in slide.Content.OfType()) { - content.AppendLine(text.Text.ToString()); - content.AppendLine(); + content.AppendLineLF(text.Text.ToString()); + content.AppendLineLF(); } foreach (var image in slide.Content.OfType()) { - content.AppendLine(image.MarkdownImage); - content.AppendLine(); + content.AppendLineLF(image.MarkdownImage); + content.AppendLineLF(); } } diff --git a/app/MindWork AI Studio/Tools/SourceExtensions.cs b/app/MindWork AI Studio/Tools/SourceExtensions.cs index f7d9e5ffe..8657aa36d 100644 --- a/app/MindWork AI Studio/Tools/SourceExtensions.cs +++ b/app/MindWork AI Studio/Tools/SourceExtensions.cs @@ -226,10 +226,10 @@ public static partial class SourceExtensions foreach (var group in sources.GroupSources()) { if (sb.Length > 0) - sb.AppendLine(); + sb.AppendLineLF(); sb.Append("## "); - sb.AppendLine(group.Heading); + sb.AppendLineLF(group.Heading); foreach (var numberedSource in group.Sources) { @@ -241,7 +241,7 @@ public static partial class SourceExtensions else AppendMarkdownLink(sb, numberedSource.Source.Title, keepPageAnchors ? numberedSource.Source.URL : WithoutPageAnchor(numberedSource.Source.URL)); - sb.AppendLine(); + sb.AppendLineLF(); } } @@ -265,7 +265,7 @@ public static partial class SourceExtensions if (string.IsNullOrWhiteSpace(sourcesMarkdown)) return string.Empty; - return $"# {TB("Sources")}{Environment.NewLine}{Environment.NewLine}{sourcesMarkdown}"; + return $"# {TB("Sources")}\n\n{sourcesMarkdown}"; } /// diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/Mailboxes/CountMailsTool.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/Mailboxes/CountMailsTool.cs index 644c62039..8f328c279 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/Mailboxes/CountMailsTool.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/Mailboxes/CountMailsTool.cs @@ -89,11 +89,11 @@ public sealed class CountMailsTool(SettingsManager settingsManager, MailboxRetri internal static ToolFunctionDefinition DescribeMailboxes(ToolFunctionDefinition function, IReadOnlyList mailboxes) { var description = new StringBuilder(DESCRIPTION); - description.AppendLine(); - description.AppendLine(); - description.AppendLine($"The mailboxes you may count, by the ID to pass in {MailToolArguments.MAILBOX_IDS_ARGUMENT}:"); + description.AppendLineLF(); + description.AppendLineLF(); + description.AppendLineLF($"The mailboxes you may count, by the ID to pass in {MailToolArguments.MAILBOX_IDS_ARGUMENT}:"); foreach (var mailbox in mailboxes) - description.AppendLine($"- id={mailbox.Id}, name='{mailbox.Name}'"); + description.AppendLineLF($"- id={mailbox.Id}, name='{mailbox.Name}'"); return function with { diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/Mailboxes/MailToolResults.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/Mailboxes/MailToolResults.cs index 60b713332..b0d528102 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/Mailboxes/MailToolResults.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/Mailboxes/MailToolResults.cs @@ -54,9 +54,9 @@ internal static class MailToolResults if (!string.IsNullOrWhiteSpace(text)) { - builder.AppendLine(); - builder.AppendLine("---"); - builder.AppendLine(); + builder.AppendLineLF(); + builder.AppendLineLF("---"); + builder.AppendLineLF(); builder.Append(text); } @@ -65,7 +65,7 @@ internal static class MailToolResults void AppendField(string label, string? value) { if (!string.IsNullOrWhiteSpace(value)) - builder.AppendLine($"- **{label}:** {value}"); + builder.AppendLineLF($"- **{label}:** {value}"); } } diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/Mailboxes/SearchMailsTool.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/Mailboxes/SearchMailsTool.cs index f7e36af96..5fe8302d9 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/Mailboxes/SearchMailsTool.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/Mailboxes/SearchMailsTool.cs @@ -135,11 +135,11 @@ public sealed class SearchMailsTool(SettingsManager settingsManager, MailboxRetr internal static ToolFunctionDefinition DescribeMailboxes(ToolFunctionDefinition function, IReadOnlyList mailboxes) { var description = new StringBuilder(DESCRIPTION); - description.AppendLine(); - description.AppendLine(); - description.AppendLine($"The mailboxes you may search, by the ID to pass in {MailToolArguments.MAILBOX_IDS_ARGUMENT}:"); + description.AppendLineLF(); + description.AppendLineLF(); + description.AppendLineLF($"The mailboxes you may search, by the ID to pass in {MailToolArguments.MAILBOX_IDS_ARGUMENT}:"); foreach (var mailbox in mailboxes) - description.AppendLine($"- id={mailbox.Id}, name='{mailbox.Name}'"); + description.AppendLineLF($"- id={mailbox.Id}, name='{mailbox.Name}'"); return function with { diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/SemanticSearch/SemanticSearchTool.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/SemanticSearch/SemanticSearchTool.cs index a2f36e8d6..81f59a1c3 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/SemanticSearch/SemanticSearchTool.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/SemanticSearch/SemanticSearchTool.cs @@ -286,21 +286,21 @@ public sealed class SemanticSearchTool(SettingsManager settingsManager, DataSour internal static ToolFunctionDefinition DescribeDataSources(ToolFunctionDefinition function, IReadOnlyList<(IDataSource DataSource, string Description)> dataSources, IReadOnlyList? unreachableDataSources = null) { var description = new StringBuilder(DESCRIPTION); - description.AppendLine(); - description.AppendLine(); - description.AppendLine($"The data sources you may search, by the ID to pass in {DATA_SOURCE_IDS_ARGUMENT}:"); + description.AppendLineLF(); + description.AppendLineLF(); + description.AppendLineLF($"The data sources you may search, by the ID to pass in {DATA_SOURCE_IDS_ARGUMENT}:"); foreach (var (dataSource, dataSourceDescription) in dataSources) { description.Append($"- id={dataSource.Id}, name='{dataSource.Name}', type={GetKind(dataSource)}"); if (!string.IsNullOrWhiteSpace(dataSourceDescription)) description.Append($", description='{dataSourceDescription.Trim().Shorten(MAX_DESCRIPTION_CHARACTERS)}'"); - description.AppendLine(); + description.AppendLineLF(); } if (unreachableDataSources is { Count: > 0 }) { - description.AppendLine(); + description.AppendLineLF(); UnreachableDataSources.AppendTo(description, unreachableDataSources); } diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSelectionRules.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSelectionRules.cs index 5cd16727f..e32940768 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSelectionRules.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSelectionRules.cs @@ -50,7 +50,7 @@ public static class ToolSelectionRules public static string BuildToolPolicyPrompt(IEnumerable definitions) { var policySections = GetInstructedTools(definitions) - .Select(x => $"## Tool `{x.Function.Name}`{Environment.NewLine}{x.SystemPromptInstructions.Trim()}") + .Select(x => $"## Tool `{x.Function.Name}`\n{x.SystemPromptInstructions.Trim()}") .Distinct(StringComparer.Ordinal) .ToList(); if (policySections.Count == 0) @@ -60,7 +60,7 @@ public static class ToolSelectionRules # Tool usage instructions: You have multiple tools available. Each tool has a different purpose and usage policy. Choose wisely and if you are not sure, always ask the user for clarification. You must follow the usage policy of each tool to ensure accurate and reliable results. Here are the usage policies for each tool: - {string.Join(Environment.NewLine+Environment.NewLine, policySections)} + {string.Join("\n\n", policySections)} """; return toolPolicyPrompt; diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSettingsService.Export.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSettingsService.Export.cs index facb94eae..6cd4aeae7 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSettingsService.Export.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSettingsService.Export.cs @@ -159,7 +159,7 @@ public sealed partial class ToolSettingsService // dictionary rather than this tool's entry. A comment says the same thing without // overwriting anything: // - lua.AppendLine($"-- The whole table is locked unless you set CONFIG[\"SETTINGS\"][\"{MINIMUM_CONFIDENCE}.AllowUserOverride\"] = true"); + lua.AppendLineLF($"-- The whole table is locked unless you set CONFIG[\"SETTINGS\"][\"{MINIMUM_CONFIDENCE}.AllowUserOverride\"] = true"); } return new(LuaCode: lua.ToString()); @@ -177,9 +177,9 @@ public sealed partial class ToolSettingsService var table = $"CONFIG[\"SETTINGS\"][\"{settingName}\"]"; if (lua.Length > 0) - lua.AppendLine(); - lua.AppendLine($"{table} = {table} or {{}}"); + lua.AppendLineLF(); + lua.AppendLineLF($"{table} = {table} or {{}}"); foreach (var (key, value) in values) - lua.AppendLine($"{table}[\"{LuaTools.EscapeLuaString(key)}\"] = \"{LuaTools.EscapeLuaString(value)}\""); + lua.AppendLineLF($"{table}[\"{LuaTools.EscapeLuaString(key)}\"] = \"{LuaTools.EscapeLuaString(value)}\""); } } \ No newline at end of file diff --git a/app/SharedTools/LuaTable.cs b/app/SharedTools/LuaTable.cs index 06d4883d5..a0c6d110e 100644 --- a/app/SharedTools/LuaTable.cs +++ b/app/SharedTools/LuaTable.cs @@ -1,5 +1,7 @@ using System.Text; +using SharedTools.Text; + namespace SharedTools; public static class LuaTable @@ -9,7 +11,7 @@ public static class LuaTable // // Add the UI_TEXT_CONTENT table: // - sb.AppendLine($$"""{{tableVariableName}} = {}"""); + sb.AppendLineLF($$"""{{tableVariableName}} = {}"""); foreach (var kvp in keyValuePairs.OrderBy(x => x.Key)) { if (cancellationToken.IsCancellationRequested) @@ -32,11 +34,11 @@ public static class LuaTable keyWithoutPrefix = keyWithoutPrefix.Replace(".", "::"); // Add a comment with the original text content: - sb.AppendLine(); - sb.AppendLine($"-- {commentValue}"); + sb.AppendLineLF(); + sb.AppendLineLF($"-- {commentValue}"); // Add the assignment to the UI_TEXT_CONTENT table: - sb.AppendLine($""" + sb.AppendLineLF($""" UI_TEXT_CONTENT["{keyWithoutPrefix}"] = "{LuaTools.EscapeLuaString(value)}" """); } diff --git a/app/SharedTools/SharedTools.csproj b/app/SharedTools/SharedTools.csproj index 5b9f85a09..013151021 100644 --- a/app/SharedTools/SharedTools.csproj +++ b/app/SharedTools/SharedTools.csproj @@ -7,4 +7,8 @@ enable + + + + diff --git a/app/SharedTools/StringBuilderExtensions.cs b/app/SharedTools/Text/StringBuilderExtensions.cs similarity index 98% rename from app/SharedTools/StringBuilderExtensions.cs rename to app/SharedTools/Text/StringBuilderExtensions.cs index aad6a8b5c..59eee60e0 100644 --- a/app/SharedTools/StringBuilderExtensions.cs +++ b/app/SharedTools/Text/StringBuilderExtensions.cs @@ -1,7 +1,7 @@ using System.Runtime.CompilerServices; using System.Text; -namespace SharedTools; +namespace SharedTools.Text; /// /// Appends lines with LF, the same line break on every system. diff --git a/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md b/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md index 006aa3013..aa9f4f49f 100644 --- a/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md +++ b/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md @@ -21,3 +21,4 @@ MWAIS0015 | Usage | Error | JsonSourceGenerationAnalyzer MWAIS0016 | Style | Error | ClassicExtensionMethodAnalyzer MWAIS0017 | Usage | Error | IconCatalogAnalyzer + MWAIS0018 | Usage | Error | SystemLineBreakAnalyzer diff --git a/app/SourceCodeRules/SourceCodeRules/Identifier.cs b/app/SourceCodeRules/SourceCodeRules/Identifier.cs index 0bbcea361..dba8ab0ce 100644 --- a/app/SourceCodeRules/SourceCodeRules/Identifier.cs +++ b/app/SourceCodeRules/SourceCodeRules/Identifier.cs @@ -19,4 +19,5 @@ public static class Identifier public const string JSON_SOURCE_GENERATION_ANALYZER = $"{Tools.ID_PREFIX}0015"; public const string CLASSIC_EXTENSION_METHOD_ANALYZER = $"{Tools.ID_PREFIX}0016"; public const string ICON_CATALOG_ANALYZER = $"{Tools.ID_PREFIX}0017"; + public const string SYSTEM_LINE_BREAK_ANALYZER = $"{Tools.ID_PREFIX}0018"; } \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/SystemLineBreakAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/SystemLineBreakAnalyzer.cs new file mode 100644 index 000000000..345ad4e1d --- /dev/null +++ b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/SystemLineBreakAnalyzer.cs @@ -0,0 +1,85 @@ +using System.Collections.Immutable; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace SourceCodeRules.UsageAnalyzers; + +/// +/// Refuses the line break of the system: StringBuilder.AppendLine and Environment.NewLine. +/// +/// +/// Both write CR LF on Windows and LF everywhere else. A text built with them differs between the systems, it mixes +/// two kinds of line breaks as soon as one of its parts comes with LF, and a test which expects LF fails on Windows +/// only, where no pipeline runs the tests. AppendLineLF of SharedTools.Text and '\n' write LF on every system. Where a +/// format needs CR LF, such as a row of a CSV file, write it on purpose, as CsvWriter.ROW_END does. +/// +#pragma warning disable RS1038 +[DiagnosticAnalyzer(LanguageNames.CSharp)] +#pragma warning restore RS1038 +public sealed class SystemLineBreakAnalyzer : DiagnosticAnalyzer +{ + /// + /// The key of the properties of a diagnostic which tells the code fix what was found. + /// + public const string KIND_KEY = "Kind"; + + /// + /// The kind of a diagnostic about a call of StringBuilder.AppendLine. + /// + public const string KIND_APPEND_LINE = "AppendLine"; + + /// + /// The kind of a diagnostic about Environment.NewLine. + /// + public const string KIND_NEW_LINE = "NewLine"; + + private const string DIAGNOSTIC_ID = Identifier.SYSTEM_LINE_BREAK_ANALYZER; + + private const string TITLE = "Line breaks are LF on every system"; + + private const string MESSAGE_FORMAT = "'{0}' writes the line break of the system, which is CR LF on Windows; use {1} instead"; + + private const string DESCRIPTION = "StringBuilder.AppendLine and Environment.NewLine write CR LF on Windows and LF elsewhere, so a text differs between the systems and mixes two kinds of line breaks. Use AppendLineLF of SharedTools.Text or '\\n'."; + + private const string CATEGORY = "Usage"; + + private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION); + + private static readonly ImmutableDictionary APPEND_LINE_PROPERTIES = ImmutableDictionary.Empty.Add(KIND_KEY, KIND_APPEND_LINE); + + private static readonly ImmutableDictionary NEW_LINE_PROPERTIES = ImmutableDictionary.Empty.Add(KIND_KEY, KIND_NEW_LINE); + + public override ImmutableArray SupportedDiagnostics => [RULE]; + + public override void Initialize(AnalysisContext context) + { + // + // Razor markup ends up in generated code, and so does the code block of a .razor file. We analyze generated + // code as well, and the Razor compiler maps the diagnostic back to the line of the .razor file: + // + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation); + context.RegisterOperationAction(AnalyzePropertyReference, OperationKind.PropertyReference); + } + + private static void AnalyzeInvocation(OperationAnalysisContext context) + { + var invocation = (IInvocationOperation)context.Operation; + if (invocation.TargetMethod.Name is not "AppendLine" || invocation.TargetMethod.ContainingType.ToDisplayString() is not "System.Text.StringBuilder") + return; + + context.ReportDiagnostic(Diagnostic.Create(RULE, invocation.Syntax.GetLocation(), APPEND_LINE_PROPERTIES, "StringBuilder.AppendLine", "AppendLineLF of SharedTools.Text")); + } + + private static void AnalyzePropertyReference(OperationAnalysisContext context) + { + var reference = (IPropertyReferenceOperation)context.Operation; + if (reference.Property.Name is not "NewLine" || reference.Property.ContainingType.ToDisplayString() is not "System.Environment") + return; + + context.ReportDiagnostic(Diagnostic.Create(RULE, reference.Syntax.GetLocation(), NEW_LINE_PROPERTIES, "Environment.NewLine", "'\\n'")); + } +} \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/UsageCodeFixes/SystemLineBreakCodeFixProvider.cs b/app/SourceCodeRules/SourceCodeRules/UsageCodeFixes/SystemLineBreakCodeFixProvider.cs new file mode 100644 index 000000000..787955425 --- /dev/null +++ b/app/SourceCodeRules/SourceCodeRules/UsageCodeFixes/SystemLineBreakCodeFixProvider.cs @@ -0,0 +1,112 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +using SourceCodeRules.UsageAnalyzers; + +namespace SourceCodeRules.UsageCodeFixes; + +/// +/// Writes LF instead of the line break of the system: AppendLineLF instead of StringBuilder.AppendLine, and "\n" +/// instead of Environment.NewLine. +/// +/// +/// The app imports SharedTools.Text everywhere. Elsewhere, such as in SharedTools or in the tests, the fix adds the +/// using directive itself when AppendLineLF would not be found without it. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SystemLineBreakCodeFixProvider)), Shared] +public sealed class SystemLineBreakCodeFixProvider : CodeFixProvider +{ + private const string TITLE_APPEND_LINE = "Use AppendLineLF"; + + private const string TITLE_NEW_LINE = "Use a string with LF"; + + private const string NAMESPACE_OF_APPEND_LINE_LF = "SharedTools.Text"; + + public override ImmutableArray FixableDiagnosticIds => [Identifier.SYSTEM_LINE_BREAK_ANALYZER]; + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root is null) + return; + + var diagnostic = context.Diagnostics.First(); + if (!diagnostic.Properties.TryGetValue(SystemLineBreakAnalyzer.KIND_KEY, out var kind)) + return; + + var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + if (kind is SystemLineBreakAnalyzer.KIND_APPEND_LINE && NameOfCall(node.FirstAncestorOrSelf()) is { } name) + context.RegisterCodeFix(CodeAction.Create(TITLE_APPEND_LINE, token => UseAppendLineLFAsync(context.Document, name, token), TITLE_APPEND_LINE), diagnostic); + else if (kind is SystemLineBreakAnalyzer.KIND_NEW_LINE && node is ExpressionSyntax newLine) + context.RegisterCodeFix(CodeAction.Create(TITLE_NEW_LINE, token => UseLineFeedAsync(context.Document, newLine, token), TITLE_NEW_LINE), diagnostic); + } + + /// + /// Gets the name of the method a call invokes, also behind a ?. of a conditional access. + /// + /// The call, or null. + /// The name, or null when the call names no member. + private static SimpleNameSyntax? NameOfCall(InvocationExpressionSyntax? call) => call?.Expression switch + { + MemberAccessExpressionSyntax access => access.Name, + MemberBindingExpressionSyntax binding => binding.Name, + _ => null + }; + + private static async Task UseAppendLineLFAsync(Document document, SimpleNameSyntax name, CancellationToken token) + { + var root = await document.GetSyntaxRootAsync(token).ConfigureAwait(false); + if (root is null) + return document; + + var annotation = new SyntaxAnnotation(); + var renamed = document.WithSyntaxRoot(root.ReplaceNode(name, SyntaxFactory.IdentifierName("AppendLineLF").WithTriviaFrom(name).WithAdditionalAnnotations(annotation))); + var renamedRoot = await renamed.GetSyntaxRootAsync(token).ConfigureAwait(false); + var model = await renamed.GetSemanticModelAsync(token).ConfigureAwait(false); + if (renamedRoot is not CompilationUnitSyntax unit || model is null) + return renamed; + + // + // Without the namespace of AppendLineLF in scope, the call binds to nothing, and only then is the using + // directive missing. It ends with the line break the file already uses: + // + var call = unit.GetAnnotatedNodes(annotation).First().FirstAncestorOrSelf(); + if (call is null || model.GetSymbolInfo(call, token).Symbol is not null) + return renamed; + + var directive = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(NAMESPACE_OF_APPEND_LINE_LF).WithLeadingTrivia(SyntaxFactory.Space)).WithTrailingTrivia(SyntaxFactory.EndOfLine(LineBreakOf(unit))); + return renamed.WithSyntaxRoot(unit.AddUsings(directive)); + } + + private static async Task UseLineFeedAsync(Document document, ExpressionSyntax newLine, CancellationToken token) + { + var root = await document.GetSyntaxRootAsync(token).ConfigureAwait(false); + if (root is null) + return document; + + var lineFeed = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal("\n")).WithTriviaFrom(newLine); + return document.WithSyntaxRoot(root.ReplaceNode(newLine, lineFeed)); + } + + /// + /// Gets the line break a file uses, so that an added line does not bring a second kind into it. + /// + /// The root of the file. + /// The first line break of the file, or LF when it has none. + private static string LineBreakOf(SyntaxNode root) + { + var lineBreak = root.DescendantTrivia().FirstOrDefault(trivia => trivia.IsKind(SyntaxKind.EndOfLineTrivia)); + return lineBreak.IsKind(SyntaxKind.EndOfLineTrivia) ? lineBreak.ToString() : "\n"; + } +} \ No newline at end of file diff --git a/app/Tests/Analyzers/SystemLineBreakAnalyzerTests.cs b/app/Tests/Analyzers/SystemLineBreakAnalyzerTests.cs new file mode 100644 index 000000000..d79595947 --- /dev/null +++ b/app/Tests/Analyzers/SystemLineBreakAnalyzerTests.cs @@ -0,0 +1,110 @@ +using AIStudio.Tests.Models.Generation; + +using SourceCodeRules.UsageAnalyzers; +using SourceCodeRules.UsageCodeFixes; + +namespace AIStudio.Tests.Analyzers; + +/// +/// Checks that MWAIS0018 refuses the line break of the system and that its code fix writes LF instead. +/// +/// +/// StringBuilder.AppendLine and Environment.NewLine write CR LF on Windows. The pipeline runs the tests on Linux +/// only, where both write LF, so a text which differs on Windows shows up nowhere but on the computer of a developer. +/// +[TestFixture] +public sealed class SystemLineBreakAnalyzerTests +{ + private const string USINGS = """ + using System; + using System.Text; + """; + + private const string USINGS_WITH_APPEND_LINE_LF = """ + using System; + using System.Text; + using SharedTools.Text; + """; + + [TestCase("return new StringBuilder().AppendLine().ToString();", "StringBuilder.AppendLine", TestName = "AppendLine without a text")] + [TestCase("""return new StringBuilder().AppendLine("Text").ToString();""", "StringBuilder.AppendLine", TestName = "AppendLine with a text")] + [TestCase("""return new StringBuilder().AppendLine($"Number {number}").ToString();""", "StringBuilder.AppendLine", TestName = "AppendLine with an interpolated text")] + [TestCase("""return $"First{Environment.NewLine}Second";""", "Environment.NewLine", TestName = "Environment.NewLine in an interpolated text")] + [TestCase("return string.Join(Environment.NewLine, items);", "Environment.NewLine", TestName = "Environment.NewLine as a separator")] + [TestCase("""return "First" + System.Environment.NewLine;""", "Environment.NewLine", TestName = "Environment.NewLine with its namespace")] + public async Task TheLineBreakOfTheSystemIsRefused(string statement, string name) + { + var reported = await CompilationHarness.AnalyzeAsync(ClassWith(USINGS, statement), new SystemLineBreakAnalyzer()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(reported, Has.Count.EqualTo(1)); + Assert.That(reported.FirstOrDefault()?.Id, Is.EqualTo("MWAIS0018")); + Assert.That(reported.FirstOrDefault()?.GetMessage(), Does.Contain($"'{name}'")); + } + } + + [TestCase("""return new StringBuilder().AppendLineLF("Text").ToString();""", TestName = "AppendLineLF")] + [TestCase("""return new StringBuilder().Append("Text").Append('\n').ToString();""", TestName = "An LF of its own")] + [TestCase("""return new Lines().AppendLine("Text");""", TestName = "A method AppendLine of another type")] + [TestCase("return Console.Out.NewLine;", TestName = "The line break of a writer")] + public async Task AnythingElseIsLeftAlone(string statement) + { + var reported = await CompilationHarness.AnalyzeAsync(ClassWith(USINGS_WITH_APPEND_LINE_LF, statement), new SystemLineBreakAnalyzer()); + + Assert.That(reported, Is.Empty); + } + + [Test] + public async Task TheCodeFixUsesAppendLineLFWhereItIsInScope() + { + var source = ClassWith(USINGS_WITH_APPEND_LINE_LF, """return new StringBuilder().AppendLine("Text").ToString();"""); + var expected = ClassWith(USINGS_WITH_APPEND_LINE_LF, """return new StringBuilder().AppendLineLF("Text").ToString();"""); + + var fixedSource = await CompilationHarness.FixAsync(source, new SystemLineBreakAnalyzer(), new SystemLineBreakCodeFixProvider()); + + Assert.That(fixedSource.ReplaceLineEndings("\n"), Is.EqualTo(expected.ReplaceLineEndings("\n")), "The using directive is there already, so the fix renames the call and adds nothing."); + } + + [Test] + public async Task TheCodeFixAddsTheUsingDirectiveWhereItIsMissing() + { + var source = ClassWith(USINGS, """return new StringBuilder().AppendLine($"Number {number}").ToString();"""); + var expected = ClassWith(USINGS_WITH_APPEND_LINE_LF, """return new StringBuilder().AppendLineLF($"Number {number}").ToString();"""); + + var fixedSource = await CompilationHarness.FixAsync(source, new SystemLineBreakAnalyzer(), new SystemLineBreakCodeFixProvider()); + + Assert.That(fixedSource.ReplaceLineEndings("\n"), Is.EqualTo(expected.ReplaceLineEndings("\n"))); + } + + [Test] + public async Task TheCodeFixWritesAStringWithLfForEnvironmentNewLine() + { + var source = ClassWith(USINGS, "return string.Join(Environment.NewLine, items);"); + var expected = ClassWith(USINGS, """return string.Join("\n", items);"""); + + var fixedSource = await CompilationHarness.FixAsync(source, new SystemLineBreakAnalyzer(), new SystemLineBreakCodeFixProvider()); + + Assert.That(fixedSource.ReplaceLineEndings("\n"), Is.EqualTo(expected.ReplaceLineEndings("\n"))); + } + + private static string ClassWith(string usings, string statement) => + $$""" + {{usings}} + + namespace Sample; + + public sealed class Lines + { + public string AppendLine(string text) => text; + } + + public static class Texts + { + public static string Build(string[] items, int number) + { + {{statement}} + } + } + """; +} \ No newline at end of file diff --git a/app/Tests/Chat/ChatThreadEditTests.cs b/app/Tests/Chat/ChatThreadEditTests.cs index 9c14ee36f..1b0a37364 100644 --- a/app/Tests/Chat/ChatThreadEditTests.cs +++ b/app/Tests/Chat/ChatThreadEditTests.cs @@ -141,7 +141,7 @@ public sealed class ChatThreadEditTests Directory.CreateDirectory(transcriptDirectory); var path = Path.Combine(transcriptDirectory, $"{stem}-transcript-0001.md"); - File.WriteAllText(path, $"# Transcription: {stem}.mp4{Environment.NewLine}{Environment.NewLine}Hello and welcome."); + File.WriteAllText(path, $"# Transcription: {stem}.mp4\n\nHello and welcome."); return new ManagedTranscriptAttachment(Path.GetFileName(path), path, new FileInfo(path).Length, $"{stem}.mp4", false); } diff --git a/app/Tests/Chat/ContentExtensionsTests.cs b/app/Tests/Chat/ContentExtensionsTests.cs index cdc1ca01d..f6910e452 100644 --- a/app/Tests/Chat/ContentExtensionsTests.cs +++ b/app/Tests/Chat/ContentExtensionsTests.cs @@ -224,7 +224,7 @@ public sealed class ContentExtensionsTests using (Assert.EnterMultipleScope()) { Assert.That(exported, Does.StartWith(file.Content), "The document stays as the model wrote it."); - Assert.That(appended.Split(Environment.NewLine), Has.All.StartWith("%"), "A line LaTeX would read could stop the whole run."); + Assert.That(appended.Split('\n'), Has.All.StartWith("%"), "A line LaTeX would read could stop the whole run."); Assert.That(appended, Does.Contain(TOOL_SOURCE.URL)); } } @@ -347,5 +347,5 @@ public sealed class ContentExtensionsTests .Select(block => block is HeadingBlock heading ? $"h{heading.Level}" : block.GetType().Name) .ToList(); - private static string Lines(params string[] lines) => string.Join(Environment.NewLine, lines); + private static string Lines(params string[] lines) => string.Join('\n', lines); } \ No newline at end of file diff --git a/app/Tests/Chat/MarkdownRenderPlannerTests.cs b/app/Tests/Chat/MarkdownRenderPlannerTests.cs index b36bb4f3c..c82703e94 100644 --- a/app/Tests/Chat/MarkdownRenderPlannerTests.cs +++ b/app/Tests/Chat/MarkdownRenderPlannerTests.cs @@ -100,7 +100,7 @@ public sealed class MarkdownRenderPlannerTests { var parts = Parts("

Hi

"); - Assert.That(parts, Is.EqualTo(new[] { $"MARKDOWN: ```html{Environment.NewLine}

Hi

{Environment.NewLine}```" })); + Assert.That(parts, Is.EqualTo(new[] { "MARKDOWN: ```html\n

Hi

\n```" })); } [Test] diff --git a/app/Tests/Components/ExportableMarkdownTests.cs b/app/Tests/Components/ExportableMarkdownTests.cs index 93e69b56b..53cb7db7c 100644 --- a/app/Tests/Components/ExportableMarkdownTests.cs +++ b/app/Tests/Components/ExportableMarkdownTests.cs @@ -92,5 +92,5 @@ public sealed class ExportableMarkdownTests private static MarkdownDocument Render(string answer) => Markdig.Markdown.Parse(answer, Markdown.CHAT_MARKDOWN_PIPELINE); - private static string Lines(params string[] lines) => string.Join(Environment.NewLine, lines); + private static string Lines(params string[] lines) => string.Join('\n', lines); } \ No newline at end of file diff --git a/app/Tests/Models/CapabilityCharacterizationTests.cs b/app/Tests/Models/CapabilityCharacterizationTests.cs index bd470e565..3823e55ed 100644 --- a/app/Tests/Models/CapabilityCharacterizationTests.cs +++ b/app/Tests/Models/CapabilityCharacterizationTests.cs @@ -58,7 +58,7 @@ public sealed class CapabilityCharacterizationTests } File.WriteAllText(CapabilitySnapshot.ACTUAL_FILE_PATH, current); - Assert.Fail($"The capabilities of {DescribeDifference(recorded, current)}{Environment.NewLine}{Environment.NewLine}The full result was written to {CapabilitySnapshot.ACTUAL_FILE_PATH}."); + Assert.Fail($"The capabilities of {DescribeDifference(recorded, current)}\n\nThe full result was written to {CapabilitySnapshot.ACTUAL_FILE_PATH}."); } /// @@ -76,19 +76,19 @@ public sealed class CapabilityCharacterizationTests var added = currentLines.Keys.Except(recordedLines.Keys).ToList(); var removed = recordedLines.Keys.Except(currentLines.Keys).ToList(); - var message = new StringBuilder($"{changed.Count} model(s) changed, {added.Count} came into the corpus, {removed.Count} left it:").Append(Environment.NewLine); + var message = new StringBuilder($"{changed.Count} model(s) changed, {added.Count} came into the corpus, {removed.Count} left it:").Append('\n'); foreach (var model in changed.Take(LINES_SHOWN)) { - message.Append(Environment.NewLine).Append(" ").Append(model); - message.Append(Environment.NewLine).Append(" was: ").Append(recordedLines[model]); - message.Append(Environment.NewLine).Append(" now: ").Append(currentLines[model]); + message.Append('\n').Append(" ").Append(model); + message.Append('\n').Append(" was: ").Append(recordedLines[model]); + message.Append('\n').Append(" now: ").Append(currentLines[model]); } foreach (var model in added.Take(LINES_SHOWN)) - message.Append(Environment.NewLine).Append(" + ").Append(model).Append(": ").Append(currentLines[model]); + message.Append('\n').Append(" + ").Append(model).Append(": ").Append(currentLines[model]); foreach (var model in removed.Take(LINES_SHOWN)) - message.Append(Environment.NewLine).Append(" - ").Append(model).Append(": ").Append(recordedLines[model]); + message.Append('\n').Append(" - ").Append(model).Append(": ").Append(recordedLines[model]); return message.ToString(); } diff --git a/app/Tests/Models/Generation/CompilationHarness.cs b/app/Tests/Models/Generation/CompilationHarness.cs index aa5adbf05..1bcd254ac 100644 --- a/app/Tests/Models/Generation/CompilationHarness.cs +++ b/app/Tests/Models/Generation/CompilationHarness.cs @@ -55,7 +55,7 @@ public static class CompilationHarness .Where(diagnostic => diagnostic.Severity is DiagnosticSeverity.Error) .Select(diagnostic => diagnostic.ToString()); - return string.Join(Environment.NewLine, errors); + return string.Join('\n', errors); } /// diff --git a/app/Tests/Serialization/JsonContractDescription.cs b/app/Tests/Serialization/JsonContractDescription.cs index 5980b58cc..d8c055ba1 100644 --- a/app/Tests/Serialization/JsonContractDescription.cs +++ b/app/Tests/Serialization/JsonContractDescription.cs @@ -2,6 +2,8 @@ using System.Text; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; +using SharedTools.Text; + namespace AIStudio.Tests.Serialization; /// @@ -30,20 +32,20 @@ public static class JsonContractDescription : $"converter {ConverterName(typeInfo.Converter)}, created {(typeInfo.CreateObject is null ? "otherwise" : "without arguments")}"; var text = new StringBuilder(); - text.AppendLine($"{TypeName(typeInfo.Type)}: {typeInfo.Kind}, {creation}"); - text.AppendLine($" handling: numbers {typeInfo.NumberHandling}, unmapped members {typeInfo.UnmappedMemberHandling}, object creation {typeInfo.PreferredPropertyObjectCreationHandling}"); - text.AppendLine($" callbacks: serializing {typeInfo.OnSerializing is not null}, serialized {typeInfo.OnSerialized is not null}, deserializing {typeInfo.OnDeserializing is not null}, deserialized {typeInfo.OnDeserialized is not null}"); + text.AppendLineLF($"{TypeName(typeInfo.Type)}: {typeInfo.Kind}, {creation}"); + text.AppendLineLF($" handling: numbers {typeInfo.NumberHandling}, unmapped members {typeInfo.UnmappedMemberHandling}, object creation {typeInfo.PreferredPropertyObjectCreationHandling}"); + text.AppendLineLF($" callbacks: serializing {typeInfo.OnSerializing is not null}, serialized {typeInfo.OnSerialized is not null}, deserializing {typeInfo.OnDeserializing is not null}, deserialized {typeInfo.OnDeserialized is not null}"); if (typeInfo.KeyType is { } keyType) - text.AppendLine($" key: {TypeName(keyType)}"); + text.AppendLineLF($" key: {TypeName(keyType)}"); if (typeInfo.ElementType is { } elementType) - text.AppendLine($" element: {TypeName(elementType)}"); + text.AppendLineLF($" element: {TypeName(elementType)}"); if (typeInfo.PolymorphismOptions is { } polymorphism) { - text.AppendLine($" polymorphism: discriminator {polymorphism.TypeDiscriminatorPropertyName}, unrecognized ignored {polymorphism.IgnoreUnrecognizedTypeDiscriminators}, unknown types {polymorphism.UnknownDerivedTypeHandling}"); + text.AppendLineLF($" polymorphism: discriminator {polymorphism.TypeDiscriminatorPropertyName}, unrecognized ignored {polymorphism.IgnoreUnrecognizedTypeDiscriminators}, unknown types {polymorphism.UnknownDerivedTypeHandling}"); foreach (var derivedType in polymorphism.DerivedTypes) - text.AppendLine($" derived: {TypeName(derivedType.DerivedType)} as {derivedType.TypeDiscriminator}"); + text.AppendLineLF($" derived: {TypeName(derivedType.DerivedType)} as {derivedType.TypeDiscriminator}"); } foreach (var property in typeInfo.Properties) @@ -53,7 +55,7 @@ public static class JsonContractDescription // if (IsIgnored(property)) { - text.AppendLine($" property {property.Name}: ignored"); + text.AppendLineLF($" property {property.Name}: ignored"); continue; } @@ -61,7 +63,7 @@ public static class JsonContractDescription ?$"set {property.Set is not null || property.AssociatedParameter is not null}" : $"set {property.Set is not null}, constructor parameter {property.AssociatedParameter?.Name ?? "none"}"; - text.AppendLine($" property {property.Name}: {TypeName(property.PropertyType)}, order {property.Order}, get {property.Get is not null}, {setting}, required {property.IsRequired}, extension data {property.IsExtensionData}, conditional {property.ShouldSerialize is not null}, converter {ConverterName(property.CustomConverter)}, nullable get {property.IsGetNullable}, nullable set {property.IsSetNullable}, numbers {property.NumberHandling}, object creation {property.ObjectCreationHandling}"); + text.AppendLineLF($" property {property.Name}: {TypeName(property.PropertyType)}, order {property.Order}, get {property.Get is not null}, {setting}, required {property.IsRequired}, extension data {property.IsExtensionData}, conditional {property.ShouldSerialize is not null}, converter {ConverterName(property.CustomConverter)}, nullable get {property.IsGetNullable}, nullable set {property.IsSetNullable}, numbers {property.NumberHandling}, object creation {property.ObjectCreationHandling}"); } return text.ToString(); diff --git a/app/Tests/Settings/UserProvidedAPIKeyOptionsTests.cs b/app/Tests/Settings/UserProvidedAPIKeyOptionsTests.cs index 759bc104b..8186ae86e 100644 --- a/app/Tests/Settings/UserProvidedAPIKeyOptionsTests.cs +++ b/app/Tests/Settings/UserProvidedAPIKeyOptionsTests.cs @@ -77,7 +77,7 @@ public sealed class UserProvidedAPIKeyOptionsTests var descriptor = PluginConfigurationObjectDescriptors.Get(type); var section = descriptor.LuaTableName; - var optionLines = string.Join(Environment.NewLine, options.Select(option => $"CONFIG[\"{section}\"][1]{option}")); + var optionLines = string.Join('\n', options.Select(option => $"CONFIG[\"{section}\"][1]{option}")); var state = LuaState.Create(); state.OpenBasicLibrary(); diff --git a/app/Tests/Tests.csproj b/app/Tests/Tests.csproj index 5c1c8d5d7..f937ebf79 100644 --- a/app/Tests/Tests.csproj +++ b/app/Tests/Tests.csproj @@ -37,15 +37,24 @@ - + + + + $(NoWarn);MWAIS0001;MWAIS0002;MWAIS0003;MWAIS0004;MWAIS0005;MWAIS0006;MWAIS0007;MWAIS0008;MWAIS0009;MWAIS0010;MWAIS0011;MWAIS0012;MWAIS0013;MWAIS0014;MWAIS0015;MWAIS0016;MWAIS0017 + + diff --git a/app/Tests/Tools/AIGeneratedContentDisclosureServiceTests.cs b/app/Tests/Tools/AIGeneratedContentDisclosureServiceTests.cs index 72bce01dc..cf338898e 100644 --- a/app/Tests/Tools/AIGeneratedContentDisclosureServiceTests.cs +++ b/app/Tests/Tools/AIGeneratedContentDisclosureServiceTests.cs @@ -78,5 +78,5 @@ public sealed class AIGeneratedContentDisclosureServiceTests private static SettingsManager CreateSettingsManager() => new(NullLogger.Instance, null!); - private static string Lines(params string[] lines) => string.Join(Environment.NewLine, lines); + private static string Lines(params string[] lines) => string.Join('\n', lines); } diff --git a/app/Tests/Tools/ClassicRagPassagesTests.cs b/app/Tests/Tools/ClassicRagPassagesTests.cs index 53a459ecb..4a0e21a55 100644 --- a/app/Tests/Tools/ClassicRagPassagesTests.cs +++ b/app/Tests/Tools/ClassicRagPassagesTests.cs @@ -99,8 +99,8 @@ public sealed class ClassicRagPassagesTests using (Assert.EnterMultipleScope()) { Assert.That(augmentation.UsedContexts.Select(context => context.Context), Is.EqualTo(new IRetrievalContext[] { handbook, manual }), "Without the check, the AI receives every context, in the order it was retrieved."); - Assert.That(augmentation.ChatThread.AugmentedData, Does.Contain($"# Retrieval context 1 of 2{Environment.NewLine}")); - Assert.That(augmentation.ChatThread.AugmentedData, Does.Contain($"# Retrieval context 2 of 2{Environment.NewLine}")); + Assert.That(augmentation.ChatThread.AugmentedData, Does.Contain("# Retrieval context 1 of 2\n")); + Assert.That(augmentation.ChatThread.AugmentedData, Does.Contain("# Retrieval context 2 of 2\n")); Assert.That(this.runtime.FilteredBatches, Has.Count.EqualTo(1), "All contexts are filtered together, in one request."); Assert.That(this.runtime.FilteredTexts, Has.Count.EqualTo(4), "Each description and each matched text is filtered once, and nothing a second time on its own."); } @@ -124,9 +124,9 @@ public sealed class ClassicRagPassagesTests { Assert.That(augmentation.UsedContexts.Select(context => context.Context), Is.EqualTo(new IRetrievalContext[] { handbookIntroduction, manual }), "The best passage of each data source fits; the second best of the handbook does not."); Assert.That(augmentation.PassageCounts, Is.EqualTo(new[] { 1, 1 })); - Assert.That(augmentation.ChatThread.AugmentedData, Does.Contain($"# Retrieval context 2 of 2{Environment.NewLine}")); + Assert.That(augmentation.ChatThread.AugmentedData, Does.Contain("# Retrieval context 2 of 2\n")); Assert.That(augmentation.ChatThread.AugmentedData, Does.Not.Contain("The results of the handbook.")); - Assert.That(augmentation.ChatThread.AugmentedData, Does.EndWith($"1 did not fit into this request and was left out. An answer based on the data sources may therefore be incomplete.{Environment.NewLine}"), "The model learns that it did not read everything the search found."); + Assert.That(augmentation.ChatThread.AugmentedData, Does.EndWith("1 did not fit into this request and was left out. An answer based on the data sources may therefore be incomplete.\n"), "The model learns that it did not read everything the search found."); Assert.That(warnings.Messages, Has.Count.EqualTo(1)); Assert.That(warnings.Messages.SingleOrDefault(), Does.Contain("had to do without 1 of them"), "The user learns about it with the answer."); } diff --git a/app/Tests/Tools/FileExportFormatTests.cs b/app/Tests/Tools/FileExportFormatTests.cs index 307f8a01f..218334492 100644 --- a/app/Tests/Tools/FileExportFormatTests.cs +++ b/app/Tests/Tools/FileExportFormatTests.cs @@ -161,7 +161,7 @@ public sealed class FileExportFormatTests using (Assert.EnterMultipleScope()) { Assert.That(found, Is.True); - Assert.That(comment.Split(Environment.NewLine), Is.EqualTo(new[] { "% # Sources", "%", "% - [1] A title with 100 % and a_b" }), "LaTeX has no end of a comment, only the end of a line."); + Assert.That(comment.Split('\n'), Is.EqualTo(new[] { "% # Sources", "%", "% - [1] A title with 100 % and a_b" }), "LaTeX has no end of a comment, only the end of a line."); } } @@ -177,5 +177,5 @@ public sealed class FileExportFormatTests } } - private static string Lines(params string[] lines) => string.Join(Environment.NewLine, lines); + private static string Lines(params string[] lines) => string.Join('\n', lines); } \ No newline at end of file diff --git a/app/Tests/Tools/MarkdownTests.cs b/app/Tests/Tools/MarkdownTests.cs index bd7bb9e1b..4a5f08560 100644 --- a/app/Tests/Tools/MarkdownTests.cs +++ b/app/Tests/Tools/MarkdownTests.cs @@ -93,5 +93,5 @@ public sealed class MarkdownTests Assert.That(Markdown.AppendBlock(string.Empty, "**Notice**"), Is.EqualTo("**Notice**"), "With nothing above it, a blank line would only start the text with an empty line."); } - private static string Lines(params string[] lines) => string.Join(Environment.NewLine, lines); + private static string Lines(params string[] lines) => string.Join('\n', lines); } \ No newline at end of file diff --git a/app/Tests/Tools/PlainFileExportTests.cs b/app/Tests/Tools/PlainFileExportTests.cs index d603e13a0..8c1b7ca72 100644 --- a/app/Tests/Tools/PlainFileExportTests.cs +++ b/app/Tests/Tools/PlainFileExportTests.cs @@ -268,7 +268,7 @@ public sealed class PlainFileExportTests "```"); var files = PlainFileExport.ExtractFiles(MarkdownRenderPlanner.Build(answer), ','); - var partStart = answer.LastIndexOf("$$", StringComparison.Ordinal) + "$$".Length + Environment.NewLine.Length; + var partStart = answer.LastIndexOf("$$", StringComparison.Ordinal) + "$$\n".Length; var part = Markdig.Markdown.Parse(answer[partStart..], Markdown.CHAT_MARKDOWN_PIPELINE); var blockOfThePart = part.Descendants().Single(); @@ -280,7 +280,19 @@ public sealed class PlainFileExportTests } } - private static string Lines(params string[] lines) => string.Join(Environment.NewLine, lines); + [Test] + public void EveryRowOfATableEndsWithCrLfAsCsvAsksFor() + { + var files = PlainFileExport.ExtractFiles(MarkdownRenderPlanner.Build(Lines("| Quarter | Revenue |", "|---|---|", "| Q1 | 100 |")), ','); + + using (Assert.EnterMultipleScope()) + { + Assert.That(files, Has.Count.EqualTo(1)); + Assert.That(files.FirstOrDefault()?.Content, Is.EqualTo("Quarter,Revenue\r\nQ1,100\r\n"), "RFC 4180 ends every row with CR LF, on every system."); + } + } + + private static string Lines(params string[] lines) => string.Join('\n', lines); /// /// Reads a file from the fixtures next to this test. diff --git a/app/Tests/Tools/PluginSystem/ManagedSettingsCoverageTests.cs b/app/Tests/Tools/PluginSystem/ManagedSettingsCoverageTests.cs index f6c983a25..bf8d07ee0 100644 --- a/app/Tests/Tools/PluginSystem/ManagedSettingsCoverageTests.cs +++ b/app/Tests/Tools/PluginSystem/ManagedSettingsCoverageTests.cs @@ -9,6 +9,8 @@ using AIStudio.Tools.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; +using SharedTools.Text; + namespace AIStudio.Tests.Tools.PluginSystem; /// @@ -204,7 +206,7 @@ public sealed class ManagedSettingsCoverageTests """); foreach (var setting in ManagedConfiguration.RegisteredSettings) - code.AppendLine($"""CONFIG["SETTINGS"]["{setting.SettingName}"] = {ToLuaValue(setting)}"""); + code.AppendLineLF($"""CONFIG["SETTINGS"]["{setting.SettingName}"] = {ToLuaValue(setting)}"""); var plugin = await PluginFactory.Load(null, code.ToString()); if (plugin is not PluginConfiguration { IsValid: true } configurationPlugin) diff --git a/app/Tests/Tools/RelativeDataPathsMigrationTests.cs b/app/Tests/Tools/RelativeDataPathsMigrationTests.cs index d033fb568..4f7d5cc40 100644 --- a/app/Tests/Tools/RelativeDataPathsMigrationTests.cs +++ b/app/Tests/Tools/RelativeDataPathsMigrationTests.cs @@ -89,7 +89,7 @@ public sealed class RelativeDataPathsMigrationTests Directory.CreateDirectory(transcriptDirectory); var transcriptPath = Path.Combine(transcriptDirectory, "meeting-transcript-0001.md"); - File.WriteAllText(transcriptPath, $"# Transcription: meeting.mp4{Environment.NewLine}{Environment.NewLine}Hello and welcome."); + File.WriteAllText(transcriptPath, $"# Transcription: meeting.mp4\n\nHello and welcome."); var transcript = new ManagedTranscriptAttachment("meeting-transcript-0001.md", transcriptPath, new FileInfo(transcriptPath).Length, "meeting.mp4", false); var chat = new ChatThread diff --git a/app/Tests/Tools/RetrievalContextPreparationTests.cs b/app/Tests/Tools/RetrievalContextPreparationTests.cs index 142fed0ee..7a279665f 100644 --- a/app/Tests/Tools/RetrievalContextPreparationTests.cs +++ b/app/Tests/Tools/RetrievalContextPreparationTests.cs @@ -9,6 +9,8 @@ using AIStudio.Tools.Services; using Microsoft.Extensions.Logging.Abstractions; +using SharedTools.Text; + namespace AIStudio.Tests.Tools; /// @@ -87,7 +89,7 @@ public sealed class RetrievalContextPreparationTests "", ]; - Assert.That(prepared.Single().ToMarkdown(2, 5), Is.EqualTo(string.Join(Environment.NewLine, expectedLines) + Environment.NewLine), "The model reads the passages in a layout it was prompted for, so preparing them must not change it."); + Assert.That(prepared.Single().ToMarkdown(2, 5), Is.EqualTo(string.Join('\n', expectedLines) + "\n"), "The model reads the passages in a layout it was prompted for, so preparing them must not change it."); } [Test] @@ -97,9 +99,9 @@ public sealed class RetrievalContextPreparationTests using (Assert.EnterMultipleScope()) { - Assert.That(prepared.ToMarkdown(3), Does.StartWith($"# Retrieval context 3{Environment.NewLine}Data source name:"), "Semantic Search numbers its passages without knowing how many fit its budget."); - Assert.That(prepared.ToMarkdown(), Does.StartWith($"# Retrieval context{Environment.NewLine}Data source name:"), "A context standing alone has no number."); - Assert.That(prepared.ToMarkdown(2, 5), Is.EqualTo($"# Retrieval context 2 of 5{Environment.NewLine}{prepared.Body}{Environment.NewLine}"), "Below its header, a context is its body."); + Assert.That(prepared.ToMarkdown(3), Does.StartWith("# Retrieval context 3\nData source name:"), "Semantic Search numbers its passages without knowing how many fit its budget."); + Assert.That(prepared.ToMarkdown(), Does.StartWith("# Retrieval context\nData source name:"), "A context standing alone has no number."); + Assert.That(prepared.ToMarkdown(2, 5), Is.EqualTo($"# Retrieval context 2 of 5\n{prepared.Body}\n"), "Below its header, a context is its body."); } } @@ -119,10 +121,10 @@ public sealed class RetrievalContextPreparationTests var prepared = (await this.PrepareAsync(image)).Single(); var expected = new StringBuilder(); - expected.AppendLine("# Retrieval context"); + expected.AppendLineLF("# Retrieval context"); RetrievalContextExtensions.AppendContextDescription(expected, image); RetrievalContextExtensions.AppendImageMatch(expected); - expected.AppendLine(); + expected.AppendLineLF(); using (Assert.EnterMultipleScope()) { diff --git a/app/Tests/Tools/SourceExtensionsTests.cs b/app/Tests/Tools/SourceExtensionsTests.cs index fd5099f2a..afbd29b9f 100644 --- a/app/Tests/Tools/SourceExtensionsTests.cs +++ b/app/Tests/Tools/SourceExtensionsTests.cs @@ -296,7 +296,7 @@ public sealed class SourceExtensionsTests /// The Markdown of the sources. /// The entries, in the order they stand in. private static IReadOnlyList EntriesOf(string markdown) => markdown - .Split(Environment.NewLine, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) .Where(line => line.StartsWith("- [", StringComparison.Ordinal)) .ToList(); } \ No newline at end of file diff --git a/app/Tests/Tools/StringBuilderExtensionsTests.cs b/app/Tests/Tools/StringBuilderExtensionsTests.cs index c9a61d852..d94877fb3 100644 --- a/app/Tests/Tools/StringBuilderExtensionsTests.cs +++ b/app/Tests/Tools/StringBuilderExtensionsTests.cs @@ -1,7 +1,7 @@ using System.Globalization; using System.Text; -using SharedTools; +using SharedTools.Text; namespace AIStudio.Tests.Tools; diff --git a/app/Tests/Tools/ToolCalling/SearchMailsToolTests.cs b/app/Tests/Tools/ToolCalling/SearchMailsToolTests.cs index eb9ae94b1..0d8784f95 100644 --- a/app/Tests/Tools/ToolCalling/SearchMailsToolTests.cs +++ b/app/Tests/Tools/ToolCalling/SearchMailsToolTests.cs @@ -139,7 +139,7 @@ public sealed class SearchMailsToolTests : ToolRegistryTestBase using (Assert.EnterMultipleScope()) { - Assert.That(function.DescriptionForLLM, Does.Contain($"- id={WORK.Id}, name='Work'{Environment.NewLine}")); + Assert.That(function.DescriptionForLLM, Does.Contain($"- id={WORK.Id}, name='Work'\n")); Assert.That(function.DescriptionForLLM, Does.EndWith($"- id={PRIVATE.Id}, name='Private'")); Assert.That(function.DescriptionForLLM, Does.Not.Contain("per page").And.Not.Contain("last page"), "How many mails a page holds depends on how long they are, so each result tells whether a further page holds more."); Assert.That(properties[MailToolArguments.MAILBOX_IDS_ARGUMENT]!["items"]!["enum"]!.AsArray().Select(id => id!.GetValue()), Is.EqualTo(new[] { WORK.Id, PRIVATE.Id })); diff --git a/app/Tests/Tools/ToolCalling/SemanticSearchToolPagingTests.cs b/app/Tests/Tools/ToolCalling/SemanticSearchToolPagingTests.cs index 1d3d47f69..f767d609d 100644 --- a/app/Tests/Tools/ToolCalling/SemanticSearchToolPagingTests.cs +++ b/app/Tests/Tools/ToolCalling/SemanticSearchToolPagingTests.cs @@ -132,7 +132,7 @@ public sealed partial class SemanticSearchToolPagingTests { var result = await this.SearchAsync([HANDBOOK], 2, Found(Passages(1, 10, LONG))); - Assert.That(result.TextContent, Does.StartWith($"# Retrieval context 4{Environment.NewLine}"), "The model reads the fourth passage of the search, not the first one a second time."); + Assert.That(result.TextContent, Does.StartWith("# Retrieval context 4\n"), "The model reads the fourth passage of the search, not the first one a second time."); } [Test] @@ -230,7 +230,7 @@ public sealed partial class SemanticSearchToolPagingTests Assert.That(shown.Text, Is.EqualTo(passage.MatchedText), "The matched text is what the search found, so it is the last to be shortened."); Assert.That(shown.SurroundingContent[0], Is.EqualTo(passage.SurroundingContent[0])); Assert.That(shown.SurroundingContent[^1], Does.StartWith("After ").And.Length.LessThan(LENGTH), "The surrounding content is shortened from its end."); - Assert.That(result.TextContent, Does.Contain($"Matched text content:{Environment.NewLine}").And.Contain("Surrounding text content, shortened to fit the size limit:")); + Assert.That(result.TextContent, Does.Contain("Matched text content:\n").And.Contain("Surrounding text content, shortened to fit the size limit:")); Assert.That(result.TextContent.Length, Is.LessThanOrEqualTo(SemanticSearchTool.MAX_RESULT_CHARACTERS)); } } diff --git a/app/Tests/Tools/ToolCalling/SemanticSearchToolRequestTests.cs b/app/Tests/Tools/ToolCalling/SemanticSearchToolRequestTests.cs index d3b6698dd..d7da480d3 100644 --- a/app/Tests/Tools/ToolCalling/SemanticSearchToolRequestTests.cs +++ b/app/Tests/Tools/ToolCalling/SemanticSearchToolRequestTests.cs @@ -64,7 +64,7 @@ public sealed class SemanticSearchToolRequestTests [Test] public void AQueryOfSeveralLinesIsRefused() { - var message = Refusal(() => SemanticSearchTool.ReadRequest(Query($"travel expenses{Environment.NewLine}hotels"), OFFERED)); + var message = Refusal(() => SemanticSearchTool.ReadRequest(Query("travel expenses\nhotels"), OFFERED)); Assert.That(message, Does.Contain("'query'").And.Contain("single line")); } diff --git a/app/Tests/Tools/WorkspaceChatCopyTests.cs b/app/Tests/Tools/WorkspaceChatCopyTests.cs index fbda91528..20251d65f 100644 --- a/app/Tests/Tools/WorkspaceChatCopyTests.cs +++ b/app/Tests/Tools/WorkspaceChatCopyTests.cs @@ -84,7 +84,7 @@ public sealed class WorkspaceChatCopyTests Directory.CreateDirectory(transcriptDirectory); var path = Path.Combine(transcriptDirectory, $"{stem}-transcript-0001.md"); - File.WriteAllText(path, $"# Transcription: {stem}.mp4{Environment.NewLine}{Environment.NewLine}Hello and welcome."); + File.WriteAllText(path, $"# Transcription: {stem}.mp4\n\nHello and welcome."); return new ManagedTranscriptAttachment(Path.GetFileName(path), path, new FileInfo(path).Length, $"{stem}.mp4", false); } } \ No newline at end of file diff --git a/app/Tests/Tools/WorkspaceChatMoveTests.cs b/app/Tests/Tools/WorkspaceChatMoveTests.cs index e5cf0ee46..4abf3433d 100644 --- a/app/Tests/Tools/WorkspaceChatMoveTests.cs +++ b/app/Tests/Tools/WorkspaceChatMoveTests.cs @@ -187,7 +187,7 @@ public sealed class WorkspaceChatMoveTests var fileName = $"{stem}-transcript-{number:D4}.md"; var path = Path.Combine(transcriptDirectory, fileName); - File.WriteAllText(path, $"# Transcription: {stem}.mp4{Environment.NewLine}{Environment.NewLine}Hello and welcome."); + File.WriteAllText(path, $"# Transcription: {stem}.mp4\n\nHello and welcome."); return new ManagedTranscriptAttachment(fileName, path, new FileInfo(path).Length, $"{stem}.mp4", false); } } \ No newline at end of file