Background
PR #802 wired the SPA confirm-and-retry UX for the redaction guard. For
/api/upload, when the user cancels the bypass dialog, the two
upload entry points behave inconsistently and both leave the UI in a
stale state if any files were already persisted on the first pass.
This issue covers both sites in one fix because they share the same
state-refresh and selection-cleanup logic.
Affected sites
A. Upload tab (packages/memtomem/src/memtomem/web/static/app.js ~line 4082)
const upload = await uploadFilesWithRedactionRetry(form);
const data = upload.data;
show(result);
result.innerHTML = '';
data.files.forEach(r => { /* render rows */ });
if (upload.cancelled) {
showToast(t('toast.upload_redaction_cancelled', { count: upload.blockedFileCount }), 'error');
return; // ← early return skips _markDataStale() / refresh / clear
}
On cancel, per-file rows render, the cancel toast renders, but:
_markDataStale() is never called → header counts / source stats stay
cached from before the upload.
selectedFiles is not cleared → the same files remain in the picker.
If the user retries with a different surface (e.g. paste into Add),
the upload picker still lists them.
B. Search-tab drag-drop (app.js ~line 5594)
const upload = await uploadFilesWithRedactionRetry(fd);
if (upload.cancelled) return; // ← silent: no toast, no rendering
const data = upload.data;
On cancel, the drop-zone shows nothing — no toast, no per-file error
display. The earlier toast.indexing_files toast is still on screen,
which actively misleads (suggests success). Same _markDataStale() gap
as site A for any files that were written on the first pass.
Reproduction
mm web with at least one indexed source already populated (so the
stats panel has a non-zero baseline to compare against).
- Site A: Index → Upload → drop a mixed batch (1 clean + 1 secret).
- First pass: clean file persists, secret file returns blocked.
- Cancel the bypass dialog.
- Observe: header / source stats unchanged despite a new file having
been written; selectedFiles still populated.
- Repeat for site B by drag-dropping onto the Search-tab drop-zone.
Note: no cancel toast at all.
Proposed fix
In both sites, on upload.cancelled:
- If
upload.data.files contains any successful writes (indexed_chunks > 0
or no error), call _markDataStale() and clear those entries from
selectedFiles (site A) before returning.
- Show a uniform cancel toast at site B (currently only site A has one).
- Keep the blocked entries visible / re-attachable so the user can
modify and retry without re-selecting.
A small helper — say _finalizeUploadCancel({data, blockedCount}) —
would let both sites share the cleanup logic and avoid drift.
Test plan
- Playwright spec extension: cancel path on mixed batch asserts
_markDataStale() was called (data-stale class on header, or
follow-up /api/stats refetch fires).
- Site B spec: cancel after mixed-batch drop on Search drop-zone fires a
cancel toast.
- Manual
mm web smoke: refresh-on-cancel makes the new clean-file
count visible without a manual reload.
Out of scope
Refs PR #802, PR #784, issue #785.
Background
PR #802 wired the SPA confirm-and-retry UX for the redaction guard. For
/api/upload, when the user cancels the bypass dialog, the twoupload entry points behave inconsistently and both leave the UI in a
stale state if any files were already persisted on the first pass.
This issue covers both sites in one fix because they share the same
state-refresh and selection-cleanup logic.
Affected sites
A. Upload tab (
packages/memtomem/src/memtomem/web/static/app.js~line 4082)On cancel, per-file rows render, the cancel toast renders, but:
_markDataStale()is never called → header counts / source stats staycached from before the upload.
selectedFilesis not cleared → the same files remain in the picker.If the user retries with a different surface (e.g. paste into Add),
the upload picker still lists them.
B. Search-tab drag-drop (
app.js~line 5594)On cancel, the drop-zone shows nothing — no toast, no per-file error
display. The earlier
toast.indexing_filestoast is still on screen,which actively misleads (suggests success). Same
_markDataStale()gapas site A for any files that were written on the first pass.
Reproduction
mm webwith at least one indexed source already populated (so thestats panel has a non-zero baseline to compare against).
been written;
selectedFilesstill populated.Note: no cancel toast at all.
Proposed fix
In both sites, on
upload.cancelled:upload.data.filescontains any successful writes (indexed_chunks > 0or no
error), call_markDataStale()and clear those entries fromselectedFiles(site A) before returning.modify and retry without re-selecting.
A small helper — say
_finalizeUploadCancel({data, blockedCount})—would let both sites share the cleanup logic and avoid drift.
Test plan
_markDataStale()was called (data-stale class on header, orfollow-up
/api/statsrefetch fires).cancel toast.
mm websmoke: refresh-on-cancel makes the new clean-filecount visible without a manual reload.
Out of scope
follow-up to PR feat(web/redaction): SPA confirm-and-retry for redaction-blocked 403 (#785) #802; the two are orthogonal — that one fires on
confirm, this one on cancel).
Refs PR #802, PR #784, issue #785.