(feature): index keyword and meaning progress across all mailboxes

This commit is contained in:
Maksym Sadovnychyy 2026-09-20 18:03:33 +02:00
parent 7198014a21
commit adee0696ba
6 changed files with 66 additions and 24 deletions

View File

@ -6,6 +6,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic
## [Unreleased] ## [Unreleased]
## [0.3.5] - 2026-09-20
Index status covers every mailbox, not one folder or one account.
### Changed
- Status bar keyword and meaning index lines show one done / total across all mailboxes, not the folder or mailbox currently being scanned.
## [0.3.4] - 2026-09-20 ## [0.3.4] - 2026-09-20
Status bar index lines no longer flash between batches. Status bar index lines no longer flash between batches.

View File

@ -30,6 +30,11 @@
<display_length compare="ge">768</display_length> <display_length compare="ge">768</display_length>
</requires> </requires>
<releases> <releases>
<release version="0.3.5" date="2026-09-20">
<description>
<p>Keyword and meaning index status shows one done / total across all mailboxes, not the current folder or a single account.</p>
</description>
</release>
<release version="0.3.4" date="2026-09-20"> <release version="0.3.4" date="2026-09-20">
<description> <description>
<p>Status bar indexing and meaning-index lines stay visible between batches instead of flashing.</p> <p>Status bar indexing and meaning-index lines stay visible between batches instead of flashing.</p>

View File

@ -3,7 +3,7 @@
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Version>0.3.4</Version> <Version>0.3.5</Version>
<Product>Postclient</Product> <Product>Postclient</Product>
<AssemblyTitle>Postclient</AssemblyTitle> <AssemblyTitle>Postclient</AssemblyTitle>
<Authors>Maksym Sadovnychyy</Authors> <Authors>Maksym Sadovnychyy</Authors>

View File

@ -197,13 +197,21 @@ public sealed class SemanticSearchService : ISemanticSearchService {
await DelayAsync(TimeSpan.FromMilliseconds(800), token).ConfigureAwait(false); await DelayAsync(TimeSpan.FromMilliseconds(800), token).ConfigureAwait(false);
if (_archive.PendingEmbeddings(EmbeddingModelSpec.Id, 1, ready).Count > 0) if (_archive.PendingEmbeddings(EmbeddingModelSpec.Id, 1, ready).Count > 0)
continue; continue;
var leftAll = _archive.EmbeddingPendingCount(EmbeddingModelSpec.Id);
var doneAll = _archive.EmbeddingCount(EmbeddingModelSpec.Id);
if (leftAll > 0) {
SetStatus(MailIndexProgress.MeaningLine(doneAll, doneAll + leftAll), ready: true);
await WaitAsync(token).ConfigureAwait(false);
return;
}
SetStatus("", ready: true); SetStatus("", ready: true);
await WaitAsync(token).ConfigureAwait(false); await WaitAsync(token).ConfigureAwait(false);
return; return;
} }
var left = _archive.EmbeddingPendingCount(EmbeddingModelSpec.Id, ready); var left = _archive.EmbeddingPendingCount(EmbeddingModelSpec.Id);
var done = _archive.EmbeddingCount(EmbeddingModelSpec.Id, ready); var done = _archive.EmbeddingCount(EmbeddingModelSpec.Id);
SetStatus(MailIndexProgress.MeaningLine(done, done + left), ready: true); SetStatus(MailIndexProgress.MeaningLine(done, done + left), ready: true);
foreach (var item in pending) { foreach (var item in pending) {
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();

View File

@ -105,6 +105,14 @@ public sealed class MailArchiveCatalog : IDisposable {
public int MessageCount(string mailboxId) => public int MessageCount(string mailboxId) =>
OffUi(() => TryGet(mailboxId)?.MessageCount(mailboxId) ?? 0); OffUi(() => TryGet(mailboxId)?.MessageCount(mailboxId) ?? 0);
public int TotalMessageCount() =>
OffUi(() => {
var count = 0;
foreach (var pair in _stores)
count += pair.Value.MessageCount(pair.Key);
return count;
});
public int RebuildKeywordIndex(string mailboxId) => public int RebuildKeywordIndex(string mailboxId) =>
OffUi(() => TryGet(mailboxId)?.RebuildKeywordIndex() ?? 0); OffUi(() => TryGet(mailboxId)?.RebuildKeywordIndex() ?? 0);

View File

@ -38,7 +38,6 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
private readonly HashSet<string> _catalogFolders = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet<string> _catalogFolders = new(StringComparer.OrdinalIgnoreCase);
private const int IndexCatalogChunk = 2_000; private const int IndexCatalogChunk = 2_000;
private const int IndexUiChunk = 250; private const int IndexUiChunk = 250;
private int _indexBodyTotal;
private readonly StatusLineHold _indexHold; private readonly StatusLineHold _indexHold;
private readonly StatusLineHold _semanticHold; private readonly StatusLineHold _semanticHold;
private bool _uiReady; private bool _uiReady;
@ -3756,10 +3755,10 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
} }
private void EnsureIndexing(string mailboxId) { private void EnsureIndexing(string mailboxId) {
if (string.IsNullOrWhiteSpace(mailboxId)) if (!string.IsNullOrWhiteSpace(mailboxId))
return; _indexMailboxes.Add(mailboxId);
_indexMailboxes.Add(mailboxId); TrackConnectedMailboxes();
if (_backfill is not null) if (_backfill is not null || _indexMailboxes.Count == 0)
return; return;
StartIndexing(); StartIndexing();
} }
@ -3799,7 +3798,10 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
try { try {
while (!token.IsCancellationRequested) { while (!token.IsCancellationRequested) {
await WaitIfIndexingPausedAsync(token).ConfigureAwait(false); await WaitIfIndexingPausedAsync(token).ConfigureAwait(false);
var snapshot = await Dispatcher.UIThread.InvokeAsync(CaptureIndexSnapshot); var (snapshot, readyIds) = await Dispatcher.UIThread.InvokeAsync(() => {
TrackConnectedMailboxes();
return (CaptureIndexSnapshot(), ReadyMailboxIds());
});
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
if (snapshot.Count == 0) if (snapshot.Count == 0)
break; break;
@ -3824,11 +3826,6 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
pendingFolders = true; pendingFolders = true;
cataloged = true; cataloged = true;
await WaitIfIndexingPausedAsync(token).ConfigureAwait(false); await WaitIfIndexingPausedAsync(token).ConfigureAwait(false);
var folderDone = box.CatalogedFolders.Count;
await SetIndexLineAsync(
MailIndexProgress.Line(folderDone, Math.Max(box.Folders.Count, 1), folder.Name),
token)
.ConfigureAwait(false);
var known = _archive.Uids(box.Id, folder.FullName); var known = _archive.Uids(box.Id, folder.FullName);
var listed = await box.Session var listed = await box.Session
.ListMessagesAsync(folder.FullName, known, token, IndexCatalogChunk) .ListMessagesAsync(folder.FullName, known, token, IndexCatalogChunk)
@ -3837,6 +3834,8 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
continue; continue;
var sync = listed.Value ?? new MailFolderSync(); var sync = listed.Value ?? new MailFolderSync();
ApplyFolderSync(box.Id, folder.FullName, sync); ApplyFolderSync(box.Id, folder.FullName, sync);
await SetIndexLineAsync(GlobalIndexLine(), token)
.ConfigureAwait(false);
var counts = _archive.FolderCounts(box.Id, folder.FullName); var counts = _archive.FolderCounts(box.Id, folder.FullName);
await UiAsync(() => RefreshIndexedFolderCounts(box.Id, folder.FullName, counts)) await UiAsync(() => RefreshIndexedFolderCounts(box.Id, folder.FullName, counts))
.ConfigureAwait(false); .ConfigureAwait(false);
@ -3855,10 +3854,6 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
} }
await WaitIfIndexingPausedAsync(token).ConfigureAwait(false); await WaitIfIndexingPausedAsync(token).ConfigureAwait(false);
var readyIds = snapshot
.Where(s => IsInitialSyncComplete(s.Id))
.Select(s => s.Id)
.ToList();
var pending = readyIds.Count == 0 ? [] : _archive.MissingBodies(readyIds, 16); var pending = readyIds.Count == 0 ? [] : _archive.MissingBodies(readyIds, 16);
if (pending.Count == 0) { if (pending.Count == 0) {
if (!cataloged) if (!cataloged)
@ -3866,11 +3861,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
continue; continue;
} }
var left = _archive.MissingBodyCount(readyIds); await SetIndexLineAsync(GlobalIndexLine(), token)
if (_indexBodyTotal < left)
_indexBodyTotal = left;
var done = Math.Max(0, _indexBodyTotal - left);
await SetIndexLineAsync(MailIndexProgress.Line(done, _indexBodyTotal), token)
.ConfigureAwait(false); .ConfigureAwait(false);
var fetched = 0; var fetched = 0;
foreach (var item in pending) { foreach (var item in pending) {
@ -3916,7 +3907,6 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
var again = _indexMailboxes.Any(id => SessionFor(MailboxById(id)) is { IsConnected: true }); var again = _indexMailboxes.Any(id => SessionFor(MailboxById(id)) is { IsConnected: true });
if (!again) { if (!again) {
_indexHold.Set(""); _indexHold.Set("");
_indexBodyTotal = 0;
return; return;
} }
@ -3962,6 +3952,29 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
_indexHold.Set(line); _indexHold.Set(line);
}).ConfigureAwait(false); }).ConfigureAwait(false);
private void TrackConnectedMailboxes() {
foreach (var box in Mailboxes) {
if (SessionFor(box) is { IsConnected: true })
_indexMailboxes.Add(box.Id);
}
}
private List<string> ReadyMailboxIds() {
var ids = new List<string>();
foreach (var box in Mailboxes) {
if (IsInitialSyncComplete(box.Id))
ids.Add(box.Id);
}
return ids;
}
private string GlobalIndexLine() {
var total = _archive.TotalMessageCount();
var missing = _archive.MissingBodyCount();
return MailIndexProgress.Line(Math.Max(0, total - missing), total);
}
private MailboxAccount? MailboxById(string mailboxId) => private MailboxAccount? MailboxById(string mailboxId) =>
Mailboxes.FirstOrDefault(m => m.Id.Equals(mailboxId, StringComparison.OrdinalIgnoreCase)) Mailboxes.FirstOrDefault(m => m.Id.Equals(mailboxId, StringComparison.OrdinalIgnoreCase))
?? _files.Current.FindMailbox(mailboxId); ?? _files.Current.FindMailbox(mailboxId);