Improved how AI Studio saves its files (#1201)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-11-14 21:16:40 +01:00
1 parent b7255694cd
commit dc97db256e
24 files changed
+626 -258

No files matched your search

@@ -192,27 +192,9 @@
<MudPaper Outlined="true" Class="pa-4 mb-4">
<MudText Typo="Typo.h5" Class="mb-3">@T("Briefing settings")</MudText>
@*
The confidence belongs to the provider chosen right next to it, so both share one row.
It uses the icon trigger, like the chat does, so this row ends the same way the profile
row below it does: a field followed by one compact icon button.
Do not add a margin to that button to "correct" its height: a dense outlined select with
a label carries margin-top 8px and margin-bottom 4px of its own, so centring the boxes
already lands within a few pixels of the visible frame, and any added margin makes it
worse. Baseline alignment does not work here either, because the wrapper below takes
its baseline from its last line box, which sits under the input.
*@
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Wrap="Wrap.NoWrap">
@* ProviderSelection marks its select as flex-grow-0, and that utility is declared
!important, so StretchItems cannot widen it. The width has to come from here. *@
<div class="flex-grow-1">
<ProviderSelection @bind-ProviderSettings="@this.editor.Provider" ValidateProvider="@this.ValidateProvider" ExplicitMinimumConfidence="@this.MinimumProviderConfidence" Disabled="@this.IsCurrentBusy"/>
</div>
@if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence)
{
<ConfidenceInfo LLMProvider="@this.editor.Provider.UsedLLMProvider"/>
}
</MudStack>
@* The provider row ends in a field followed by one compact icon button, the same way the
profile row below it does. *@
<ProviderSelectionWithConfidence @bind-ProviderSettings="@this.editor.Provider" ValidateProvider="@this.ValidateProvider" ExplicitMinimumConfidence="@this.MinimumProviderConfidence" Disabled="@this.IsCurrentBusy"/>
<ProfileFormSelection @bind-Profile="@this.editor.Profile" Disabled="@this.IsCurrentBusy"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.Name)" @bind-Value="@this.editor.TargetLanguage" Icon="@AppIcons.LANGUAGE" Label="@T("Target language")" AllowOther="true" @bind-OtherInput="@this.editor.CustomTargetLanguage" OtherValue="CommonLanguages.OTHER" LabelOther="@T("Custom target language")" ValidateOther="@this.ValidateCustomTargetLanguage" SelectionUpdated="@(_ => this.ScheduleFormValidation())" Disabled="@this.IsCurrentBusy"/>
@@ -98,8 +98,7 @@ public sealed partial class VisualBriefingStore
return;
Directory.CreateDirectory(this.RootDirectory);
foreach (var temporaryPath in Directory.EnumerateFiles(this.RootDirectory, "*.tmp-*", SearchOption.AllDirectories))
TryDeleteFile(temporaryPath);
AtomicFile.DeleteTemporaryFiles(this.RootDirectory, logger);
await this.LoadSelectionAsync(token);
foreach (var directory in Directory.EnumerateDirectories(this.RootDirectory))
@@ -111,26 +111,22 @@ public sealed partial class VisualBriefingStore(
}
/// <summary>
/// Defines <c>WriteTextAtomicAsync</c> for the visual briefing feature.
/// Writes a text file of a briefing so that readers see either the old or the new content, never a part of it.
/// </summary>
private static async Task WriteTextAtomicAsync(string targetPath,
/// <remarks>
/// The content reaches the disk before it replaces the file, so that a power failure takes no briefing away.
/// </remarks>
/// <param name="targetPath">The file to write.</param>
/// <param name="content">The new content.</param>
/// <param name="overwrite">Whether an existing file is replaced; otherwise, an existing file fails the write with an IOException.</param>
/// <param name="token">The cancellation token.</param>
private static Task WriteTextAtomicAsync(string targetPath,
string content,
bool overwrite,
CancellationToken token)
{
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
var temporaryPath = $"{targetPath}.tmp-{Guid.NewGuid():N}";
try
{
await File.WriteAllTextAsync(temporaryPath, content, new UTF8Encoding(false), token);
await using (var stream = new FileStream(temporaryPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None, 4_096, true))
await stream.FlushAsync(token);
File.Move(temporaryPath, targetPath, overwrite);
}
finally
{
TryDeleteFile(temporaryPath);
}
return AtomicFile.WriteTextAsync(targetPath, content, new UTF8Encoding(false), token, overwrite, flushToDisk: true);
}
/// <summary>