diff --git a/app/MindWork AI Studio/Tools/DataSourceReindexWarning.cs b/app/MindWork AI Studio/Tools/DataSourceReindexWarning.cs
new file mode 100644
index 00000000..34c4a584
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/DataSourceReindexWarning.cs
@@ -0,0 +1,147 @@
+using System.Text;
+
+using AIStudio.Dialogs;
+using AIStudio.Settings;
+using AIStudio.Tools.PluginSystem;
+using AIStudio.Tools.Services;
+
+namespace AIStudio.Tools;
+
+///
+/// Asks before an edit makes the prepared documents of data sources useless.
+///
+///
+/// Kept here rather than in the dialogs which ask -- the embedding provider dialog and the two data
+/// source dialogs -- so the sentence naming what a rebuild costs cannot drift apart between them.
+/// That is the same reason DataSourceRepair sits next to it, and both name the same two costs.
+///
+/// Nothing is asked when nothing is lost. A data source only reaches the question when the edit
+/// really changes its embedding signature and when the index already holds something for it, so
+/// renaming an embedding provider or editing a data source nobody has indexed yet stays silent.
+///
+public static class DataSourceReindexWarning
+{
+ ///
+ /// How many data sources are named before the rest is only counted.
+ ///
+ private const int MAX_NAMED_DATA_SOURCES = 10;
+
+ private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(DataSourceReindexWarning).Namespace, nameof(DataSourceReindexWarning));
+
+ ///
+ /// Asks before an edited embedding provider is saved.
+ ///
+ /// The dialog service to ask with.
+ /// The settings, read for the data sources behind the provider.
+ /// The service which knows what the index holds.
+ /// The embedding provider as it is stored.
+ /// The embedding provider as it would be stored.
+ /// The cancellation token.
+ /// True when the edit may be saved.
+ public static async Task ConfirmEmbeddingProviderChangeAsync(IDialogService dialogService, SettingsManager settingsManager, DataSourceEmbeddingService embeddingService,
+ EmbeddingProvider before, EmbeddingProvider after, CancellationToken token = default)
+ {
+ // Nothing was stored under this id, so no data source can point at it:
+ if (before == EmbeddingProvider.NONE)
+ return true;
+
+ var candidates = GetDataSourcesUsing(settingsManager, before.Id)
+ .Where(dataSource => EmbeddingChangeImpact.AffectsStoredIndex(dataSource, before, after))
+ .Cast()
+ .ToList();
+
+ if (candidates.Count == 0)
+ return true;
+
+ var affected = await embeddingService.GetDataSourcesWithStoredIndexAsync(candidates, token);
+ return await ConfirmAsync(dialogService, affected, !after.IsSelfHosted);
+ }
+
+ ///
+ /// Asks before an edited data source is saved.
+ ///
+ /// The dialog service to ask with.
+ /// The settings, read for the embedding provider of the data source.
+ /// The service which knows what the index holds.
+ /// The data source as it is stored.
+ /// The data source as it would be stored.
+ /// The cancellation token.
+ /// True when the edit may be saved.
+ public static async Task ConfirmDataSourceChangeAsync(IDialogService dialogService, SettingsManager settingsManager, DataSourceEmbeddingService embeddingService,
+ IInternalDataSource before, IInternalDataSource after, CancellationToken token = default)
+ {
+ // Without a provider nothing is embedded at all, so nothing can be lost:
+ if (!DataSourceEmbeddingProviders.TryResolve(settingsManager, after, out var embeddingProvider))
+ return true;
+
+ if (!EmbeddingChangeImpact.AffectsStoredIndex(embeddingProvider, before, after))
+ return true;
+
+ var affected = await embeddingService.GetDataSourcesWithStoredIndexAsync([after], token);
+ return await ConfirmAsync(dialogService, affected, !embeddingProvider.IsSelfHosted);
+ }
+
+ ///
+ /// The data sources which are indexed with a given embedding provider.
+ ///
+ /// The settings holding the data sources.
+ /// The id of the embedding provider.
+ /// The data sources pointing at that embedding provider.
+ private static IReadOnlyList GetDataSourcesUsing(SettingsManager settingsManager, string embeddingProviderId) =>
+ settingsManager.ConfigurationData.DataSources
+ .OfType()
+ .Where(dataSource => embeddingProviderId.Equals(dataSource.EmbeddingId, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ ///
+ /// Names data sources as a Markdown list, counting the rest when there are too many to name.
+ ///
+ /// The data sources to name.
+ /// The Markdown list.
+ private static string FormatDataSourceNames(IReadOnlyList dataSources)
+ {
+ var names = dataSources
+ .Select(dataSource => dataSource.Name)
+ .OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ var lines = names.Take(MAX_NAMED_DATA_SOURCES).Select(name => $"- {name}").ToList();
+ 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);
+ }
+
+ private static async Task ConfirmAsync(IDialogService dialogService, IReadOnlyList affected, bool usesCloudEmbedding)
+ {
+ if (affected.Count == 0)
+ return true;
+
+ 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."));
+
+ if (usesCloudEmbedding)
+ {
+ body.AppendLine();
+ body.AppendLine(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?"));
+
+ var dialogParameters = new DialogParameters
+ {
+ { x => x.MarkdownBody, body.ToString() },
+ };
+
+ var dialogReference = await dialogService.ShowAsync(TB("Documents Will Be Prepared Again"), dialogParameters, Dialogs.DialogOptions.FULLSCREEN);
+ var dialogResult = await dialogReference.Result;
+ return dialogResult is not null && !dialogResult.Canceled;
+ }
+}
\ No newline at end of file