diff --git a/app/MindWork AI Studio/Tools/Databases/IndexStore/NoIndexStoreClient.cs b/app/MindWork AI Studio/Tools/Databases/IndexStore/NoIndexStoreClient.cs index 1b690f0f..305e75cf 100644 --- a/app/MindWork AI Studio/Tools/Databases/IndexStore/NoIndexStoreClient.cs +++ b/app/MindWork AI Studio/Tools/Databases/IndexStore/NoIndexStoreClient.cs @@ -26,7 +26,6 @@ public sealed class NoIndexStoreClient(string name, string? unavailableReason, D // user sees, so this is the one place where those details matter most. // yield return (TB("Native library"), OrUnknown(SqliteRuntimeInfo.GetNativeLibraryName())); - yield return (TB("Native library path"), OrUnknown(SqliteRuntimeInfo.GetNativeLibraryPath())); yield return (TB("Process architecture"), OrUnknown(SqliteRuntimeInfo.GetProcessArchitecture())); await Task.CompletedTask; diff --git a/app/MindWork AI Studio/Tools/Databases/IndexStore/SqliteIndexStoreClientImplementation.cs b/app/MindWork AI Studio/Tools/Databases/IndexStore/SqliteIndexStoreClientImplementation.cs index 2d4b7084..a164f089 100644 --- a/app/MindWork AI Studio/Tools/Databases/IndexStore/SqliteIndexStoreClientImplementation.cs +++ b/app/MindWork AI Studio/Tools/Databases/IndexStore/SqliteIndexStoreClientImplementation.cs @@ -71,9 +71,7 @@ public sealed class SqliteIndexStoreClientImplementation(string name, string dat var snapshot = await this.ReadDisplaySnapshotAsync(); yield return (TB("Reported version"), version); - yield return (TB("Library source ID"), OrUnknown(snapshot.LibrarySourceId)); yield return (TB("Native library"), OrUnknown(SqliteRuntimeInfo.GetNativeLibraryName())); - yield return (TB("Native library path"), OrNotDetermined(SqliteRuntimeInfo.GetNativeLibraryPath())); yield return (TB("Wrapper version"), OrUnknown(SqliteRuntimeInfo.GetWrapperVersion())); yield return (TB("Process architecture"), OrUnknown(SqliteRuntimeInfo.GetProcessArchitecture())); @@ -83,14 +81,12 @@ public sealed class SqliteIndexStoreClientImplementation(string name, string dat yield return (TB("System architecture"), systemArchitecture); yield return (TB("Full-text search (FTS5)"), OrUnknown(snapshot.FullTextSearch)); - yield return (TB("Database path"), this.databasePath); yield return (TB("Journal mode"), OrUnknown(snapshot.JournalMode)); yield return (TB("Schema version"), OrUnknown(snapshot.SchemaVersion)); yield return (TB("Database tables"), OrUnknown(snapshot.TableCount)); yield return (TB("Storage size"), this.GetStorageSize()); yield return (TB("Indexed data sources"), OrUnknown(snapshot.DataSourceCount)); yield return (TB("Indexed files"), OrUnknown(snapshot.FileCount)); - yield return (TB("Search chunks"), OrUnknown(snapshot.ChunkCount)); yield return (TB("Permanently skipped files"), OrUnknown(snapshot.FailureCount)); } @@ -406,8 +402,6 @@ public sealed class SqliteIndexStoreClientImplementation(string name, string dat /// private sealed record DisplaySnapshot { - public string LibrarySourceId { get; init; } = string.Empty; - public string FullTextSearch { get; init; } = string.Empty; public string JournalMode { get; init; } = string.Empty; @@ -420,15 +414,11 @@ public sealed class SqliteIndexStoreClientImplementation(string name, string dat public string FileCount { get; init; } = string.Empty; - public string ChunkCount { get; init; } = string.Empty; - public string FailureCount { get; init; } = string.Empty; } private static string OrUnknown(string value) => string.IsNullOrWhiteSpace(value) ? TB("unknown") : value; - private static string OrNotDetermined(string value) => string.IsNullOrWhiteSpace(value) ? TB("not determined") : value; - private async Task ReadDisplaySnapshotAsync() { var token = CancellationToken.None; @@ -437,14 +427,12 @@ public sealed class SqliteIndexStoreClientImplementation(string name, string dat await using var context = this.CreateContext(); return new DisplaySnapshot { - LibrarySourceId = await QueryScalarTextAsync(context, "SELECT sqlite_source_id()", token), FullTextSearch = await GetFullTextSearchStateAsync(context, token), JournalMode = (await QueryScalarTextAsync(context, "PRAGMA journal_mode;", token)).ToUpperInvariant(), SchemaVersion = await GetSchemaVersionAsync(context, token), TableCount = await GetTableCountAsync(context, token), DataSourceCount = await FormatCountAsync(context.DataSources, token), FileCount = await FormatCountAsync(context.EmbeddedFiles, token), - ChunkCount = (await this.GetTotalChunkCountAsync(token))?.ToString("N0", I18N.I.Culture) ?? string.Empty, FailureCount = await FormatCountAsync(context.PermanentIndexingFailures, token), }; } @@ -503,7 +491,7 @@ public sealed class SqliteIndexStoreClientImplementation(string name, string dat // is the point: a missing shadow table is a finding, not noise. // var tables = await QueryScalarTextAsync(context, "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'", token); - return int.TryParse(tables, NumberStyles.Integer, CultureInfo.InvariantCulture, out var tableCount) ? tableCount.ToString("N0", I18N.I.Culture) : string.Empty; + return int.TryParse(tables, NumberStyles.Integer, CultureInfo.InvariantCulture, out var tableCount) ? tableCount.CompactCount() : string.Empty; } private static async Task GetSchemaVersionAsync(IndexStoreDbContext context, CancellationToken token) @@ -521,8 +509,8 @@ public sealed class SqliteIndexStoreClientImplementation(string name, string dat var pendingMigrations = (await context.Database.GetPendingMigrationsAsync(token)).ToList(); return pendingMigrations.Count == 0 - ? string.Format(I18N.I.Culture, TB("{0} ({1} applied)"), appliedMigrations[^1], appliedMigrations.Count) - : string.Format(I18N.I.Culture, TB("{0} ({1} applied, {2} pending)"), appliedMigrations[^1], appliedMigrations.Count, pendingMigrations.Count); + ? string.Format(I18N.I.Culture, TB("{0} ({1} applied)"), appliedMigrations[^1], appliedMigrations.Count.CompactCount()) + : string.Format(I18N.I.Culture, TB("{0} ({1} applied, {2} pending)"), appliedMigrations[^1], appliedMigrations.Count.CompactCount(), pendingMigrations.Count.CompactCount()); } catch { @@ -534,7 +522,7 @@ public sealed class SqliteIndexStoreClientImplementation(string name, string dat { try { - return (await query.CountAsync(token)).ToString("N0", I18N.I.Culture); + return (await query.CountAsync(token)).CompactCount(); } catch { diff --git a/app/MindWork AI Studio/Tools/Databases/IndexStore/SqliteRuntimeInfo.cs b/app/MindWork AI Studio/Tools/Databases/IndexStore/SqliteRuntimeInfo.cs index 4ec847da..5b7ead33 100644 --- a/app/MindWork AI Studio/Tools/Databases/IndexStore/SqliteRuntimeInfo.cs +++ b/app/MindWork AI Studio/Tools/Databases/IndexStore/SqliteRuntimeInfo.cs @@ -33,42 +33,6 @@ internal static class SqliteRuntimeInfo } } - /// - /// Where the native library sits on disk. - /// - /// - /// This probes the file system instead of enumerating the loaded modules: Process.Modules throws - /// on macOS, and NativeLibrary hands out no path at all. For our single-file builds, the base - /// directory is where the runtime extracts the native assets to, so that is the first candidate. - /// - public static string GetNativeLibraryPath() - { - try - { - var libraryName = GetNativeLibraryName(); - if (string.IsNullOrWhiteSpace(libraryName)) - return string.Empty; - - var fileName = GetNativeFileName(libraryName); - var baseDirectory = AppContext.BaseDirectory; - string[] candidates = - [ - Path.Combine(baseDirectory, fileName), - Path.Combine(baseDirectory, "runtimes", RuntimeInformation.RuntimeIdentifier, "native", fileName), - ]; - - foreach (var candidate in candidates) - if (File.Exists(candidate)) - return candidate; - - return string.Empty; - } - catch - { - return string.Empty; - } - } - /// /// The version of the managed SQLitePCLRaw wrapper, which is a different thing than the SQLite version. /// @@ -121,15 +85,4 @@ internal static class SqliteRuntimeInfo return string.Empty; } } - - private static string GetNativeFileName(string libraryName) - { - if (OperatingSystem.IsWindows()) - return $"{libraryName}.dll"; - - if (OperatingSystem.IsMacOS()) - return $"lib{libraryName}.dylib"; - - return $"lib{libraryName}.so"; - } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Databases/VectorStore/QdrantEdgeClientImplementation.cs b/app/MindWork AI Studio/Tools/Databases/VectorStore/QdrantEdgeClientImplementation.cs index ea880a9b..5f045353 100644 --- a/app/MindWork AI Studio/Tools/Databases/VectorStore/QdrantEdgeClientImplementation.cs +++ b/app/MindWork AI Studio/Tools/Databases/VectorStore/QdrantEdgeClientImplementation.cs @@ -91,8 +91,8 @@ public sealed class QdrantEdgeClientImplementation( yield return (TB("Reported version"), displayVersion); yield return (TB("Storage size"), $"{this.GetStorageSize()}"); - yield return (TB("Number of vector stores"), displayStoresCount.ToString("N0", I18N.I.Culture)); - yield return (TB("Stored vectors"), storedVectors?.ToString("N0", I18N.I.Culture) ?? TB("unknown")); + yield return (TB("Number of vector stores"), displayStoresCount.CompactCount()); + yield return (TB("Stored vectors"), storedVectors?.CompactCount() ?? TB("unknown")); } /// diff --git a/app/MindWork AI Studio/Tools/LongExtensions.cs b/app/MindWork AI Studio/Tools/LongExtensions.cs index 3209e47a..660593a6 100644 --- a/app/MindWork AI Studio/Tools/LongExtensions.cs +++ b/app/MindWork AI Studio/Tools/LongExtensions.cs @@ -1,7 +1,46 @@ +using AIStudio.Tools.PluginSystem; + namespace AIStudio.Tools; public static class LongExtensions { + private static readonly string[] COUNT_SUFFIXES = ["k", "M", "B", "T"]; + + /// + /// Formats a count so that large numbers stay readable: 1456 becomes 1.46k, 4512900 becomes 4.51M. + /// + /// + /// Counts scale in thousands, not in steps of 1024 — that is what FileSize is for, and storage + /// sizes keep using it. Numbers below 1000 stay exact, because shortening them would hide the + /// difference between 4 and 999. The decimal separator follows the culture of the active language + /// plugin rather than the one of the thread, which never moves along with the app's language. + /// + /// The number to format. + /// The formatted number. + public static string CompactCount(this long count) + { + var culture = I18N.I.Culture; + if (count is > -1_000 and < 1_000) + return count.ToString("N0", culture); + + var order = -1; + double value = count; + while (Math.Abs(value) >= 1_000 && order < COUNT_SUFFIXES.Length - 1) + { + order++; + value /= 1_000; + } + + return $"{value.ToString("0.##", culture)}{COUNT_SUFFIXES[order]}"; + } + + /// + /// Formats a count so that large numbers stay readable. + /// + /// The number to format. + /// The formatted number. + public static string CompactCount(this int count) => ((long)count).CompactCount(); + /// /// Formats the file size in a human-readable format. ///