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
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("