feat: Ehud wishlist 2026-07-30 (#347) — AeroSync tab state, duplicate hashes, shared trash table - #519
Conversation
📝 WalkthroughWalkthroughChangesThe PR adds fuzzy duplicate signatures and progress reporting, a shared sortable trash table, persistent AeroSync tab state, richer scan counters, cryptographic overlay-scope persistence, native encryption badges, and duplicate-finder translations. Estimated code review effort: 5 (Critical) | ~120 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ast-grep (0.45.0)src-tauri/src/lib.rsast-grep timed out on this file Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
aaba0c0 to
a793e7d
Compare
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src-tauri/src/lib.rs (1)
12063-12172: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winEmit a final local progress tick after traversal.
Progress is emitted before each directory is processed, so entries from the final directory are never reported. This permanently undercounts local directories and bytes in the Compare UI. Emit one unthrottled final event using
files.len(),dirs_found, andbytes_foundbefore returning.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src-tauri/src/lib.rs` around lines 12063 - 12172, Update the directory traversal function containing the progress emission and final file collection return to emit one unthrottled progress event after traversal completes. Use files.len(), dirs_found, and bytes_found for the final counts, and place the event immediately before returning the completed scan result so entries from the final directory are included.
🧹 Nitpick comments (8)
src/components/DuplicateFinderDialog.tsx (2)
465-472: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExpose the progress bar to assistive tech.
The bar is a bare
div; addrole="progressbar"witharia-valuenow/aria-valuemin/aria-valuemaxso screen-reader users get the same fraction the counters show.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/DuplicateFinderDialog.tsx` around lines 465 - 472, Add progressbar accessibility attributes to the outer progress container in DuplicateFinderDialog’s analyze-phase progress UI: set role="progressbar", aria-valuenow to the same calculated percentage used for the width, and aria-valuemin/aria-valuemax to 0 and 100 respectively.
37-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
formatElapsedduplicates the helper insrc/hooks/useScanProgress.ts.The same formatter (with tests) lands in
src/hooks/useScanProgress.tsin this PR stack and is consumed bysrc/components/AeroSync/CompareTabContent.tsx. Import it here instead of re-implementing, so the two clocks cannot drift in format.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/DuplicateFinderDialog.tsx` around lines 37 - 46, Remove the local formatElapsed implementation from DuplicateFinderDialog and import and reuse the shared formatter from useScanProgress. Update its call sites to preserve the existing elapsed-time display while ensuring both components use the same tested formatting behavior.src-tauri/src/dedupe/mod.rs (1)
340-346: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueExact-mode
files_totalis the whole candidate list, not the files actually hashed.
files_total = paths.len(), but exact mode only ticks inside size groups with ≥2 members, so a caller renderingfiles_processed / files_totalwill stall well below 100% (the fuzzy arm is consistent because it ticks every path). The GUI path avoids this by computing its ownhash_totalinfilesystem.rs, so this only bites other callers offind_similar_local_with_progress(e.g. CLI). Either compute the hashable total before the loop or document the denominator's meaning per mode.♻️ Compute the hashable total for exact mode
let mut hash_groups: HashMap<String, (u64, Vec<String>)> = HashMap::new(); + let files_total = size_groups + .values() + .filter(|fs| fs.len() >= 2) + .map(|fs| fs.len() as u64) + .sum::<u64>(); for (sz, files) in size_groups {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src-tauri/src/dedupe/mod.rs` around lines 340 - 346, Update the exact-mode progress accounting in find_similar_local_with_progress so files_total represents only files that will actually be hashed—those belonging to size groups with at least two members—rather than the full candidate list. Compute this hashable total before processing groups and use it in each DedupeProgress callback, while preserving fuzzy-mode progress behavior.src-tauri/src/filesystem.rs (1)
1718-1771: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy liftOffload duplicate scanning from the async command worker.
find_duplicate_filesnow performs the directory walk, synchronous file reads, BLAKE3 hashing, and, in non-identical mode, pHash/SimHash/TLSH signing and clustering inside a Tauriasynccommand. Keep the event loop responsive by moving the scanning/computation work totauri::async_runtime::spawn_blocking(returningAppHandleprogress/callbacks to emit events). Applies here and in the non-identical analysis branch that callsfind_similar_local_with_progress.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src-tauri/src/filesystem.rs` around lines 1718 - 1771, Move the synchronous directory walk and duplicate-analysis computation in find_duplicate_files into tauri::async_runtime::spawn_blocking, preserving the existing results and progress behavior. Ensure the blocking closure owns or receives the collected paths and uses AppHandle-based callbacks to emit progress events. Apply the same offloading to the non-identical analysis path invoking find_similar_local_with_progress, including pHash/SimHash/TLSH signing and clustering.src/components/BoxTrashManager.tsx (1)
128-139: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract a shared
TrashRowmapper instead of repeating the sameitems.map(...)in 9 files. Every provider trash manager duplicates the identical normalization shape (id/name/isDir/size/deletedAt), differing only in howidis derived; a single helper intrashRows.ts(e.g.toTrashRows(items, idFn)) would remove the drift risk whenTrashRowgains/changes fields.
src/components/BoxTrashManager.tsx#L128-L139: replace withtoTrashRows(items, getItemId), wrapped inuseMemo(..., [items]).src/components/InternxtTrashManager.tsx#L61-L72: replace withtoTrashRows(items, item => item.path).src/components/JottacloudTrashManager.tsx#L118-L129: replace withtoTrashRows(items, item => item.path).src/components/MegaTrashManager.tsx#L118-L129: replace withtoTrashRows(items, item => item.name).src/components/OneDriveTrashManager.tsx#L123-L134: replace withtoTrashRows(items, getItemId).src/components/OpenDriveTrashManager.tsx#L141-L152: replace withtoTrashRows(items, itemId).src/components/PCloudTrashManager.tsx#L134-L145: replace withtoTrashRows(items, item => item.path).src/components/YandexTrashManager.tsx#L129-L140: replace withtoTrashRows(items, item => item.path).src/components/ZohoTrashManager.tsx#L124-L135: replace withtoTrashRows(items, getItemId).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/BoxTrashManager.tsx` around lines 128 - 139, Extract a shared toTrashRows helper in trashRows.ts that maps items to the common TrashRow fields and accepts an id function. Replace the duplicated mappings in src/components/BoxTrashManager.tsx:128-139, src/components/InternxtTrashManager.tsx:61-72, src/components/JottacloudTrashManager.tsx:118-129, src/components/MegaTrashManager.tsx:118-129, src/components/OneDriveTrashManager.tsx:123-134, src/components/OpenDriveTrashManager.tsx:141-152, src/components/PCloudTrashManager.tsx:134-145, src/components/YandexTrashManager.tsx:129-140, and src/components/ZohoTrashManager.tsx:124-135 with the helper inside useMemo using the existing id derivations and preserving each dependency array.src/components/Trash/trashRows.test.ts (1)
23-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider covering
deletedAtMsprecedence.
trashRowDeletedMsprefersdeletedAtMsoverdeletedAt, and NextcloudTrashManager depends on it exclusively, but no fixture here setsdeletedAtMs. A row withdeletedAtMsplus a conflicting/absentdeletedAtwould lock that contract.Also applies to: 68-73
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Trash/trashRows.test.ts` around lines 23 - 29, Extend the sortTrashRows test fixtures and assertions to cover trashRowDeletedMs precedence by adding a row with deletedAtMs and a conflicting or absent deletedAt value. Verify sorting uses deletedAtMs over deletedAt, preserving the existing coverage for rows that only provide deletedAt.src/components/Trash/TrashTable.tsx (1)
131-143: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExpose sort state to assistive tech.
The header button's
aria-labeljust repeats the visible label, so screen-reader users get no indication of which column is sorted or in which direction (the chevron is visual only). Addaria-sortto the<th>.♿ Proposed fix
- <th key={key} className={`px-2 py-1.5 ${className}`}> + <th + key={key} + aria-sort={sort?.key === key ? (sort.direction === 'asc' ? 'ascending' : 'descending') : 'none'} + className={`px-2 py-1.5 ${className}`} + > <button type="button" onClick={() => setSort((cur) => nextTrashSort(cur, key))} className="inline-flex items-center hover:text-gray-700 dark:hover:text-gray-300" - aria-label={t(labelKey) || fallback} >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Trash/TrashTable.tsx` around lines 131 - 143, Update the table header `<th>` rendered in the HEADERS mapping to include an `aria-sort` value derived from the current sort state for each `key`, indicating ascending, descending, or unsorted. Keep the existing visible labels, sort button behavior, and `sortIndicator(key)` unchanged.src/components/S3TrashManager.tsx (1)
173-250: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value
s3Columnsmemo never memoizes.
restoreis redefined on every render (Line 84), so the dependency comparison always fails and thisuseMemorecomputes each time. WraprestoreinuseCallback(its own deps:loadTrash,humanLog,onRefreshFiles) or drop the memo since the column array is cheap.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/S3TrashManager.tsx` around lines 173 - 250, Stabilize the restore function used by s3Columns by wrapping restore in useCallback with loadTrash, humanLog, and onRefreshFiles as dependencies. Keep the existing restore behavior unchanged so the s3Columns useMemo can retain its memoized columns.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src-tauri/src/filesystem.rs`:
- Around line 1800-1811: Add resource limiting to the non-identical branch
before calling find_similar_local_with_progress: enforce a cumulative byte
budget or per-file size cap consistent with exact mode, so large candidates are
excluded or the input is truncated before the engine buffers them. Update the
walked/path preparation around is_non_identical while preserving the existing
similarity mode, progress callback, and exact-mode behavior.
- Around line 1776-1798: Ensure the analysis progress emitter produces a final
100% event: after the exact-mode hashing loop and after the engine analysis
call, invoke the existing emit_analysis closure with files_total as both
progress values and force set to true. Keep throttled updates during processing
unchanged.
In `@src-tauri/src/provider_commands.rs`:
- Around line 6650-6658: The remote progress JSON currently derives dirs_found
from remote_files, whose entries all have is_dir false, so it always reports
zero. Update the final remote phase payload to reuse the actual directory count
collected by the scanner or progress observer, ensuring this tick preserves the
correct count instead of overwriting the UI value.
- Around line 6762-6774: Update the remote scan aggregation near the progress
emission and the code that inserts into remote_files to maintain deduplicated
file, directory, and byte totals as entries are added or replaced. Adjust totals
by subtracting the replaced entry’s prior contribution before adding the new
entry’s contribution, then use these maintained counters in the
sync_scan_progress payload instead of traversing remote_files values.
In `@src/components/AeroSync/AeroSyncDialog.tsx`:
- Around line 63-68: Move the dialog session tracking around tabState and
wasOpen out of the render phase in AeroSyncDialog. Replace the render-time
wasOpen.current mutation with an effect or equivalent side-effect-safe mechanism
that detects a transition from closed to open, resets the provider-owned
tabState once per fresh session, and preserves state across tab switches.
In `@src/components/DuplicateFinderDialog.tsx`:
- Around line 409-452: Update the controls in DuplicateFinderDialog to use the
existing useTranslation/t() flow for all hardcoded labels, options, and
fuzzy-cutoff helper text. Replace the Checkbox wrapper label text with the
Checkbox component’s label prop, removing the unassociated surrounding label
while preserving the current showHashes toggle behavior and styling.
In `@src/components/FileLuTrashManager.tsx`:
- Around line 120-133: Update the trashRows construction around the useMemo
callback to exclude items without a truthy file_code before mapping, matching
the existing .filter(Boolean) guards elsewhere in FileLuTrashManager. Keep only
valid file_code entries so each row has a unique id and per-row actions receive
a non-empty fileCode.
- Around line 147-166: Update the per-row action handlers in the render callback
to match restoreSelected and deleteSelected: handle restore rejections with
user-facing error state, and ensure both restore and permanent delete record the
corresponding activity log. Preserve the existing reload and file-refresh
behavior after successful operations.
In `@src/components/NextcloudTrashManager.tsx`:
- Around line 151-160: Normalize deleted_at to an unknown value when it is zero
in the trashRows mapping, so deletedAtMs is not a finite timestamp for unknown
dates and sorting places the row in the unknown group consistently with
formatDeletedDate. Update the deletedAtMs assignment within the useMemo mapping
while preserving real-date conversion.
In `@src/components/Trash/TrashTable.tsx`:
- Around line 186-188: Update the size cell in the TrashTable row rendering to
display "-" when row.isDir is true or row.size is null, matching sortTrashRows’
null-as-no-size behavior; otherwise continue formatting the numeric size with
formatSize.
---
Outside diff comments:
In `@src-tauri/src/lib.rs`:
- Around line 12063-12172: Update the directory traversal function containing
the progress emission and final file collection return to emit one unthrottled
progress event after traversal completes. Use files.len(), dirs_found, and
bytes_found for the final counts, and place the event immediately before
returning the completed scan result so entries from the final directory are
included.
---
Nitpick comments:
In `@src-tauri/src/dedupe/mod.rs`:
- Around line 340-346: Update the exact-mode progress accounting in
find_similar_local_with_progress so files_total represents only files that will
actually be hashed—those belonging to size groups with at least two
members—rather than the full candidate list. Compute this hashable total before
processing groups and use it in each DedupeProgress callback, while preserving
fuzzy-mode progress behavior.
In `@src-tauri/src/filesystem.rs`:
- Around line 1718-1771: Move the synchronous directory walk and
duplicate-analysis computation in find_duplicate_files into
tauri::async_runtime::spawn_blocking, preserving the existing results and
progress behavior. Ensure the blocking closure owns or receives the collected
paths and uses AppHandle-based callbacks to emit progress events. Apply the same
offloading to the non-identical analysis path invoking
find_similar_local_with_progress, including pHash/SimHash/TLSH signing and
clustering.
In `@src/components/BoxTrashManager.tsx`:
- Around line 128-139: Extract a shared toTrashRows helper in trashRows.ts that
maps items to the common TrashRow fields and accepts an id function. Replace the
duplicated mappings in src/components/BoxTrashManager.tsx:128-139,
src/components/InternxtTrashManager.tsx:61-72,
src/components/JottacloudTrashManager.tsx:118-129,
src/components/MegaTrashManager.tsx:118-129,
src/components/OneDriveTrashManager.tsx:123-134,
src/components/OpenDriveTrashManager.tsx:141-152,
src/components/PCloudTrashManager.tsx:134-145,
src/components/YandexTrashManager.tsx:129-140, and
src/components/ZohoTrashManager.tsx:124-135 with the helper inside useMemo using
the existing id derivations and preserving each dependency array.
In `@src/components/DuplicateFinderDialog.tsx`:
- Around line 465-472: Add progressbar accessibility attributes to the outer
progress container in DuplicateFinderDialog’s analyze-phase progress UI: set
role="progressbar", aria-valuenow to the same calculated percentage used for the
width, and aria-valuemin/aria-valuemax to 0 and 100 respectively.
- Around line 37-46: Remove the local formatElapsed implementation from
DuplicateFinderDialog and import and reuse the shared formatter from
useScanProgress. Update its call sites to preserve the existing elapsed-time
display while ensuring both components use the same tested formatting behavior.
In `@src/components/S3TrashManager.tsx`:
- Around line 173-250: Stabilize the restore function used by s3Columns by
wrapping restore in useCallback with loadTrash, humanLog, and onRefreshFiles as
dependencies. Keep the existing restore behavior unchanged so the s3Columns
useMemo can retain its memoized columns.
In `@src/components/Trash/trashRows.test.ts`:
- Around line 23-29: Extend the sortTrashRows test fixtures and assertions to
cover trashRowDeletedMs precedence by adding a row with deletedAtMs and a
conflicting or absent deletedAt value. Verify sorting uses deletedAtMs over
deletedAt, preserving the existing coverage for rows that only provide
deletedAt.
In `@src/components/Trash/TrashTable.tsx`:
- Around line 131-143: Update the table header `<th>` rendered in the HEADERS
mapping to include an `aria-sort` value derived from the current sort state for
each `key`, indicating ascending, descending, or unsorted. Keep the existing
visible labels, sort button behavior, and `sortIndicator(key)` unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 9e5e1b37-88c8-4da5-b503-d2d4adacfb17
📒 Files selected for processing (41)
src-tauri/src/dedupe/mod.rssrc-tauri/src/filesystem.rssrc-tauri/src/lib.rssrc-tauri/src/provider_commands.rssrc/App.tsxsrc/components/AeroSync/AeroSyncDialog.tsxsrc/components/AeroSync/CompareTabContent.tsxsrc/components/AeroSync/PlanTabContent.tsxsrc/components/AeroSync/SyncTabContent.tsxsrc/components/AeroSync/tabStateStore.test.tssrc/components/AeroSync/tabStateStore.tssrc/components/AzureTrashManager.tsxsrc/components/BoxTrashManager.tsxsrc/components/DropboxTrashManager.tsxsrc/components/DuplicateFinderDialog.tsxsrc/components/FileLuTrashManager.tsxsrc/components/GoogleDriveTrashManager.tsxsrc/components/InternxtTrashManager.tsxsrc/components/JottacloudTrashManager.tsxsrc/components/KDriveTrashManager.tsxsrc/components/KoofrTrashManager.tsxsrc/components/MegaTrashManager.tsxsrc/components/NextcloudTrashManager.tsxsrc/components/OneDriveTrashManager.tsxsrc/components/OpenDriveTrashManager.tsxsrc/components/PCloudTrashManager.tsxsrc/components/S3TrashManager.tsxsrc/components/SavedServers.tsxsrc/components/Trash/TrashTable.tsxsrc/components/Trash/trashRows.test.tssrc/components/Trash/trashRows.tssrc/components/YandexTrashManager.tsxsrc/components/ZohoTrashManager.tsxsrc/hooks/useScanProgress.test.tssrc/hooks/useScanProgress.tssrc/types.tssrc/types/aerofile.tssrc/utils/cryptScope.test.tssrc/utils/cryptScope.tssrc/utils/nativeE2e.test.tssrc/utils/nativeE2e.ts
|
All ten addressed in
Fuzzy path has no I/O budget — right, and it predates this PR: the engine buffered every candidate's full contents in a Always-zero clone-pool O(D×N) rescan — right. Running counters now, adjusted at the insert site by whatever the insert replaced, since the walk can revisit a path (which is why it was recomputing in the first place). Ref mutated during render — right, and the constraint that led me there was real: the store has to be empty before a tab mounts and reads it, and an effect runs after. Resetting on close satisfies it without writing a ref mid-render, since that is the same moment from the store's point of view. Hardcoded English labels — right. Five keys added and translated across all 47 locales ( FileLu null FileLu per-row actions — right, and inherited: I lifted those inline handlers out of the old list unchanged. Both now share the bulk handlers' shape, with an activity-log entry that settles and a failure the user can see. Nextcloud
Re-verified after the changes: |
Lock the overlay inside the Overlays Path, walk out to a plaintext folder, and the grey crypt toggle was still there instead of the 'Overlays Path' button that EF-02 put in its place. The badge asks isWithinCryptScope which anchor the tab is bound to, and read it from `session.cryptOverlay` — the LIVE vault, which lockSessionCryptOverlay clears. The scope then fell back to '', and '' does not mean "no anchor" to that predicate, it means "the whole remote is the anchor": every folder in the session read as inside, so the in-scope branch rendered everywhere and the jump-back button could never appear while locked. The anchor now survives the lock in `cryptOverlayScope`, exactly like `cryptOverlayKind` already does, and is dropped only on disconnect. Every markSessionOverlayKind call site passes the scope it already had in hand (binding.remoteScope / overlayHint.anchor), so the badge is scope-aware before the first unlock as well as after a lock. The predicate and the resolution move to src/utils/cryptScope.ts to be testable: the difference between "no anchor" and "anchor is the whole remote" is not observable from the rendered tree. The pin fails on the old resolution. Reported by @EhudKirsh in discussion #347.
Ehud asked how one is supposed to schedule a Plan with Update and the other
settings when they are deleted on the way to the Sync tab. They were: the
dialog renders `{activeTab === 'plan' && <PlanTabContent/>}`, so leaving the
tab unmounts it and React drops all fourteen of its useState values —
preset, direction, conflict policy, verify policy, canary, budget, streams,
compression, EC — plus the Sync tab's paths, exclude list and dry-run.
Mounting all three tabs at once would fix the state and break everything
else: the Compare tab starts a recursive scan on mount. So the tabs still
unmount, and their settings now live in a small store owned by the dialog,
read back as the initial value on remount and cleared on each fresh open.
Two effects had to learn about it. Both seed one control from another
(speed mode -> streams/compression, preset -> EC) and therefore also fire on
mount, which after a switch is a remount: they would have overwritten the
values just restored. They skip that first run and seed normally after.
Reported by @EhudKirsh in discussion #347.
…rough Both dialogs answered a long scan with a bare spinner, which is the one display that cannot distinguish work from a hang. Ehud asked for the counters that the summary shows afterwards to be shown while the user is waiting, and for a clock: "to confirm that there's no infinite loop bug". Compare needed no new backend at all. Every compare walker has emitted `sync_scan_progress` since the recursive scan landed and nothing on the frontend ever listened, so the numbers were already on the wire. The ticks now also carry the directory count and the byte total, and useScanProgress folds them: the two sides scan concurrently and the remote emitter reports a cumulative file count, so each side is replaced by its own latest tick and the totals are derived, rather than summing a number that already includes the other side. Find Duplicates had nothing to listen to, so find_duplicate_files now emits its own progress: files, directories, bytes and depth during the walk, then a determinate fraction during the analysis pass, throttled to 120ms so the reporting cannot slow the scan it reports on. Non-identical mode used to let the dedupe engine do its own walk; it now takes the file list from the same walk as exact mode, and reports its signature pass through a callback, so the two modes cannot drift and both are visible. Also in the duplicates list, per Ehud's next comment: a copy button on each file name and on each folder path, and buttons to open the file in its default app or reveal it in the file manager. The Copy glyph in there was decorative; comparing two candidates by hand meant reading the path off the screen with an OCR app. No new i18n keys: the labels reuse contextMenu.copyName, contextMenu.copyPath, contextMenu.open and aeroShare.inbox.revealFile, all already translated in 47 locales. Reported by @EhudKirsh in discussion #347.
Ehud tried the non-identical mode on his photos, found it grouping an image with the same image plus overlaid text, and asked three things of it: show the hash behind each file, order the groups from most to least similar, and let him choose the cutoff — "I will be curious to see if there is a sweet spot". Writing the sort exposed why it could not have worked. The representative distance the UI prints as "dist" was `.max()` of the SIGNATURE VALUES of the group's members, not of the hamming distances between them, on both the raster and the text arm. A pair clustered two bits apart could report a number in the billions, and sorting by it would have ordered the groups by an arbitrary 64-bit integer. It is now the largest hamming distance from the group's first member, which is bounded by the threshold that admitted it. The `other` arm was already computing a real TLSH diff. On top of that: - each group carries its members' own signatures, in file order, so a row can show why it is in the group: pHash / SimHash as 16 hex digits, TLSH as its own string. Exact mode keeps the single shared BLAKE3 it already had. A "Show hashes" toggle keeps them out of the way by default. - the cutoff reaches the engine. `find_duplicate_files` took no distance and always passed None, so the per-modality defaults were the only thing anyone could ever run; the dialog now has a Fuzzy cutoff field, blank meaning those same defaults, committed on blur or Enter rather than per keystroke. - a Sort control: wasted space (as before) or similarity, which puts the byte-identical groups first and then the fuzzy ones closest-first. Two tests: the distance pin fails on the old expression, and the threshold pin clusters a pair at a loose cutoff and separates it at zero. Reported by @EhudKirsh in discussion #347.
Add Service has shown an "E2E 128-bit" / "E2E 256-bit" chip on MEGA, Filen and Internxt for a long time. My Servers said the same thing only in prose, in the subtitle under the profile name, so a saved MEGA profile did not look encrypted at a glance. It now carries a 🔒 128-bit / 🔒 256-bit badge next to the name, alongside the transport and MEGAcmd badges already there. Both surfaces now read one list, and a test holds the Add Service catalogue to it, so they cannot drift the way they were free to before. No new i18n key: the tooltip reuses connection.filenEncryptionTitle, whose text is generic and is already translated in every locale. Ehud asked for the badge on "all the protocols of MEGA and Filen". Not all of them qualify, and the test says so out loud: MEGA S4, MEGA's paid S3-compatible object storage, is a profile with protocol 's3' where the server holds the keys. Badging it would be a false claim about who can read the files, which is exactly the distinction the badge is for. Reported by @EhudKirsh in discussion #347.
Four reports on the same surface: sort by name, size, deletion date and type; a Type column, since the dialog clearly knows a file from a folder; the Name header lining up with the names instead of reading as if it sat over the checkboxes; timestamps on one line so more rows fit; and rubber-band plus Ctrl selection "like in file managers". The obstacle was that '🗑 View Trash' is not one dialog. Seventeen provider components each carried their own copy of the same table, and `grep sortBy` across all of them returned nothing: fixing four things in seventeen places would have been sixty-eight edits and seventeen chances to diverge. So the table is now one component and every provider renders it. What each of them gets: - sortable Type / Name / Size / Deleted headers. Folders and files only split apart under Type; a folder has no size of its own, so it sorts last under Size whichever way the column points, and rows with an unparseable date sort last under Deleted. Every tie falls through to the name, so the order is total and a re-render cannot shuffle equal rows. - the type icon in its own narrow column, which is what puts the Name header above the names. - a Deleted column wide enough for a full timestamp, and never wrapped. - Shift to extend a range, Ctrl (like a plain click, these are checkboxes) to add without losing the rest, and a rubber-band drag, reusing the marquee hook written for AeroFile — whose docstring, until today, gave "trash view" as its example of a place that disables it. The awkward three came along rather than being left behind: Internxt has no restore or delete API, so it renders read-only (no checkbox column, sorting still); FileLu listed <li> rows with no size and per-row buttons, which are now extra columns; S3 lists object versions and delete markers, so it keeps its Kind and Actions columns and drops the Type one. Nextcloud keeps its original path column the same way. Eighteen tests on the sort and selection logic. No new i18n key: the Type header reuses settings.columnType, already translated everywhere. Reported by @EhudKirsh in discussion #347.
The DedupeProgress struct landed between find_similar_local's doc comment and the function, so the list in that comment continued into the struct's and clippy flagged four lazy continuations. Both items carry their own docs again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The table wrapper is not the scrolling element — each provider's dialog owns that — so hit testing is correct but the hook's edge auto-scroll reads a scrollTop that is always 0. Worth stating at the call site rather than leaving the next reader to find it by dragging. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ten findings, all of them real. Grouped by what they were: **Two the progress work got wrong.** The analysis emitter took a `force` flag and no caller ever passed true, so the throttle could swallow the last tick and leave the bar short of the total for the rest of the dialog's life; both modes now force one at the end. And the clone-pool branch of the provider compare reported `dirs_found: 0` computed from a list where every entry is `is_dir: false` by construction — a confident zero, not a count. It omits the field instead, which the reducer already treats as "keep the last known value". **One that was mine to make faster.** The recursive provider walk recomputed the directory and byte totals by traversing the whole accumulated map after every listed directory: O(directories x entries) on exactly the large trees the progress bar exists for. Running counters now, adjusted at the insert site by what the insert replaced, since a walk can revisit a path. **One that predates this PR and is worth fixing anyway.** The fuzzy engine buffered every candidate's full contents in a Vec and only then computed the signatures, so a directory of large media could hold the whole lot in memory where exact mode truncates safely under its 2 GB budget. It now reads and signs one file at a time, dropping the bytes immediately, under the same 2 GB budget. **Four in the new UI.** The five new controls in Find Duplicates were hardcoded English; they now read `duplicates.sortWasted`, `sortSimilarity`, `showHashes`, `fuzzyCutoff` and `fuzzyCutoffHint`, added and translated across all 47 locales as one-line insertions that leave each catalogue's formatting alone. The "Show hashes" `<label>` wrapped a Checkbox with no form control to point at, so clicking the text did nothing while the cursor promised otherwise: the label rides on the component now. `size: null` rendered as `0 B` for the rows that deliberately have no size (S3 delete markers, FileLu), though sortTrashRows already treated null as size-less. And Nextcloud's `deleted_at == 0` displayed as an em dash but sorted as 1970, ahead of every real date. **Two in FileLu.** Rows with a null `file_code` all became id `''`, which is a duplicate React key, one selectable row for many, and a per-row action invoked with an empty code; they are filtered out, like the two `.filter(Boolean)` call sites already in that file. And the per-row buttons I lifted out of the old list had no rejection handler on restore at all — a failure became an unhandled rejection and a stale list — and skipped the activity log the bulk handlers write. Both now share the bulk handlers' shape. **One about React.** The tab store was reset during render via a ref, which is not safe against replayed or discarded render work. It resets when the dialog closes instead: the same moment from the store's point of view, in a place where side effects belong. `tsc` clean, `vitest` 589/589, `i18n:validate` 47/47 with 0 missing and 0 placeholders, `cargo clippy --all-features --tests` with no warnings, `cargo fmt` clean, dedupe tests still green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
12be129 to
d02bcda
Compare
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/i18n/locales/en.json (1)
2730-2731: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAlign the Drime free-tier strings to 20 GB.
drimeDesccurrently says “21 GB free”, whiledrimeTooltipand other Drime docs in this repo use 20 GB. Update the description to match the provider tier before release.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/locales/en.json` around lines 2730 - 2731, Update the drimeDesc localization string to state 20 GB free, matching drimeTooltip and the repository’s documented Drime tier; leave the surrounding localization entries unchanged.src/i18n/locales/lv.json (1)
3364-3364: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winDo not claim SSE-KMS ETags are omitted.
This text contradicts
src-tauri/src/providers/s3.rs::etag_to_md5: after stripping quotes, any exact 32-ASCII-hex ETag is treated as MD5-shaped without checking encryption headers. Multipart ETags are excluded by their-Nsuffix. The current wording may cause users to incorrectly assume valid SSE-KMS checksums are unavailable.Based on learnings, S3 checksum copy must describe this as an advisory shape check rather than excluding SSE-KMS values.
Proposed wording
- "s3-etag": "ETag ir objekta MD5 tikai vienas daļas objektiem, kas nav šifrēti ar SSE-KMS; pārējiem tas tiek izlaists, nevis uzminēts.", + "s3-etag": "ETag tiek interpretēts kā MD5, ja pēc pēdiņu noņemšanas tas ir tieši 32 ASCII heksadecimālas rakstzīmes; multipart ETag tiek izlaists, taču tā ir tikai formas pārbaude, nevis garantija, ka serveris to aprēķināja kā MD5.",🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/locales/lv.json` at line 3364, Update the “s3-etag” localization text to describe ETag-to-MD5 handling as an advisory 32-character hexadecimal shape check after quote removal, with multipart ETags excluded by their “-N” suffix; do not state or imply that SSE-KMS ETags are omitted. Keep the wording consistent with etag_to_md5.Source: Learnings
src/i18n/locales/is.json (1)
1954-1954: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winTranslate the status-bar update label.
Line 1954 is still
"Update Available"in the Icelandic locale. Reuse the existing Icelandic translation,"Uppfærsla í boði!", already present at Line 675.Proposed fix
- "updateAvailable": "Update Available", + "updateAvailable": "Uppfærsla í boði!",🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/locales/is.json` at line 1954, Update the Icelandic locale’s updateAvailable value to reuse the existing translation “Uppfærsla í boði!” from the locale, replacing the remaining English label while leaving other translations unchanged.
🧹 Nitpick comments (4)
src/components/DropboxTrashManager.tsx (1)
1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSix providers repeat the same
TrashRowmapping boilerplate. Dropbox, GoogleDrive, KDrive, Mega, PCloud, and Yandex all buildtrashRowswith the identical{ name, isDir: item.is_dir, size: item.size, deletedAt: item.modified }shape, differing only in howidis derived. A shared helper (e.g. exported fromtrashRows.ts) parameterized by an id-selector would remove this duplication and keep any futureTrashRowfield additions single-sourced.
src/components/DropboxTrashManager.tsx#L167-177: replace with a call to a sharedtoTrashRows(items, getItemId)helper.src/components/GoogleDriveTrashManager.tsx#L124-134: replace with the same shared helper, passinggetItemId.src/components/KDriveTrashManager.tsx#L137-147: replace with the same shared helper, passingitem => item.path.src/components/MegaTrashManager.tsx#L120-130: replace with the same shared helper, passingitem => item.name.src/components/PCloudTrashManager.tsx#L136-146: replace with the same shared helper, passingitem => item.path.src/components/YandexTrashManager.tsx#L131-141: replace with the same shared helper, passingitem => item.path.♻️ Suggested shared helper (e.g. add to trashRows.ts)
export interface CommonTrashEntry { name: string; is_dir: boolean; size: number; modified: string | null; } export function toTrashRows<T extends CommonTrashEntry>( items: readonly T[], getId: (item: T) => string, ): TrashRow[] { return items.map(item => ({ id: getId(item), name: item.name, isDir: item.is_dir, size: item.size, deletedAt: item.modified, })); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/DropboxTrashManager.tsx` at line 1, Extract the repeated TrashRow mapping into an exported generic toTrashRows helper in trashRows.ts, using a shared CommonTrashEntry shape and an id-selector callback. Replace the trashRows mapping in DropboxTrashManager, GoogleDriveTrashManager, KDriveTrashManager, MegaTrashManager, PCloudTrashManager, and YandexTrashManager with this helper, passing each provider’s existing id derivation unchanged.src/components/S3TrashManager.tsx (1)
155-251: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
s3ColumnsuseMemo never actually memoizes —restoreis a fresh function every render.
restore(line 84) is a plain closure recreated on every render, not memoized withuseCallback. Since it's a dependency of thes3ColumnsuseMemo (line 249), the memo's dependency check always fails and the array (with its embedded JSX click handlers) is rebuilt every render, defeating the memoization.🔧 Proposed fix
- const restore = async (entry: TrashEntry, mode: RestoreMode) => { + const restore = useCallback(async (entry: TrashEntry, mode: RestoreMode) => { setActionKey(entry.version_id); setError(null); const label = mode === 'purge' ? 'purge' : mode === 'undelete' ? 'undelete' : 'restore'; const logId = humanLog.logRaw('activity.trash_restore_start', 'INFO', { provider: 'S3', count: 1 }); try { await invoke('s3_restore_from_trash', { key: entry.key, versionId: entry.version_id, mode, }); humanLog.updateEntry(logId, { status: 'success', message: `[S3] ${label} ${entry.display_key}` }); await loadTrash(); if (mode !== 'purge') onRefreshFiles?.(); } catch (err) { humanLog.updateEntry(logId, { status: 'error', message: `[S3] Failed to ${label} ${entry.display_key}` }); setError(String(err)); } finally { setActionKey(null); } - }; + }, [loadTrash, onRefreshFiles, humanLog]);Based on learnings from the static analysis hint (React Doctor
no-effect-with-fresh-deps): "Your useMemo runs every render because deprestoreis a new function built fresh each time".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/S3TrashManager.tsx` around lines 155 - 251, Memoize the restore function used by s3Columns with React’s useCallback, preserving its existing behavior and dependencies. Update the restore definition near the component’s other callbacks so its reference remains stable between renders, allowing the s3Columns useMemo dependency check to work as intended.Source: Linters/SAST tools
src/components/Trash/TrashTable.tsx (2)
118-125: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueHeader comment overstates Ctrl handling.
Lines 15-16 say "Ctrl adds without losing what is already selected", but
onRowClickforwards onlyshift; a plain click already toggles additively, so Ctrl is not actually a distinct modifier here. Either drop the Ctrl claim or make plain click replace the selection and Ctrl extend it.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Trash/TrashTable.tsx` around lines 118 - 125, The selection behavior described near onRowClick does not match the implementation: only shift is forwarded, while plain clicks already additively toggle selections. Update the header comment to remove the Ctrl-specific claim, preserving the existing selectionAfterRowClick behavior and modifier handling.
140-152: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExpose sort state to assistive tech with
aria-sort.The direction is conveyed only by the chevron glyph, and the
aria-labeljust repeats the visible button text.aria-sorton the<th>is the standard hook for sortable tables.♿ Proposed tweak
- <th key={key} className={`px-2 py-1.5 ${className}`}> + <th + key={key} + className={`px-2 py-1.5 ${className}`} + aria-sort={sort?.key === key ? (sort.direction === 'asc' ? 'ascending' : 'descending') : 'none'} + > <button type="button" onClick={() => setSort((cur) => nextTrashSort(cur, key))} className="inline-flex items-center hover:text-gray-700 dark:hover:text-gray-300" - aria-label={t(labelKey) || fallback} >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Trash/TrashTable.tsx` around lines 140 - 152, Update the header rendering in TrashTable’s HEADERS map to add aria-sort to each sortable th, deriving its value from the current sort state for that column and using the unsorted value when inactive. Keep the existing sort button behavior and label unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src-tauri/src/dedupe/mod.rs`:
- Around line 393-410: Update the signature-processing loop around the size
check to skip oversized or over-budget files with continue rather than break,
allowing later candidates to be processed. Add a per-file size cap so
std::fs::read cannot materialize a file larger than the intended memory bound,
and emit a warn! when the cumulative signature budget truncates the pass,
matching the visibility of exact mode.
In `@src-tauri/src/lib.rs`:
- Around line 12033-12036: Update the traversal completion path to
unconditionally emit a final “local” progress payload before returning,
including the accumulated files, dirs, and bytes counts. Ensure the payload uses
the final local counters so single-directory scans report directory and byte
totals even when no periodic emission occurred.
In `@src/components/DuplicateFinderDialog.tsx`:
- Around line 639-664: Update the click handlers for the open_local_file and
open_in_file_manager buttons in DuplicateFinderDialog to report invocation
failures through the dialog’s existing error channel instead of silently
swallowing them. Preserve the current best-effort behavior while passing each
caught error to the established error-reporting mechanism.
In `@src/components/FileLuTrashManager.tsx`:
- Around line 132-133: Update the deletedAtLabel assignment in the file-trash
row mapping to treat an empty result from formatDeletedAgo(item.deleted_ago_sec)
as missing, and fall back to null so TrashTable renders its "-" placeholder.
Preserve the existing deleted date label when item.deleted is present.
In `@src/i18n/locales/cy.json`:
- Line 3379: Update the sortWasted localization value in cy.json from the
ambiguous “place” wording to Welsh wording that clearly means wasted disk space,
such as “Gofod wedi'i wastraffu”.
In `@src/i18n/locales/mk.json`:
- Around line 3382-3383: Update the fuzzyCutoff translation in the Macedonian
locale to clearly describe the fuzzy/non-identical duplicate detection cutoff,
replacing the ambiguous “unclear threshold” wording; keep fuzzyCutoffHint
unchanged.
In `@src/i18n/locales/ru.json`:
- Around line 3379-3383: Add a Russian translation key for the fuzzy-cutoff
placeholder alongside fuzzyCutoffHint in the locale entries, then update
DuplicateFinderDialog.tsx to obtain that key through the existing i18n mechanism
and use it instead of the hard-coded “auto” placeholder for the fuzzy cutoff
field.
In `@src/i18n/locales/sr.json`:
- Line 3379: Update the Serbian translation for the sortWasted key to use
wording meaning “wasted/lost space,” matching the terminology of the existing
wasted key, rather than wording that means consumed or used space.
In `@src/i18n/locales/vi.json`:
- Around line 3382-3383: Update the Vietnamese fuzzyCutoff label from “Ngưỡng
mờ” to a clearer fuzzy-similarity term such as “Ngưỡng tương đồng mờ” or “Ngưỡng
so khớp mờ”, while leaving fuzzyCutoffHint unchanged.
In `@src/utils/nativeE2e.test.ts`:
- Around line 42-60: Update the entryPattern in the “the Add Service catalogue
agrees with this list” test to match across newlines and stop at each individual
catalogue entry, while still capturing its type and E2E badge. Keep the
catalogued map and both existing assertions unchanged.
---
Outside diff comments:
In `@src/i18n/locales/en.json`:
- Around line 2730-2731: Update the drimeDesc localization string to state 20 GB
free, matching drimeTooltip and the repository’s documented Drime tier; leave
the surrounding localization entries unchanged.
In `@src/i18n/locales/is.json`:
- Line 1954: Update the Icelandic locale’s updateAvailable value to reuse the
existing translation “Uppfærsla í boði!” from the locale, replacing the
remaining English label while leaving other translations unchanged.
In `@src/i18n/locales/lv.json`:
- Line 3364: Update the “s3-etag” localization text to describe ETag-to-MD5
handling as an advisory 32-character hexadecimal shape check after quote
removal, with multipart ETags excluded by their “-N” suffix; do not state or
imply that SSE-KMS ETags are omitted. Keep the wording consistent with
etag_to_md5.
---
Nitpick comments:
In `@src/components/DropboxTrashManager.tsx`:
- Line 1: Extract the repeated TrashRow mapping into an exported generic
toTrashRows helper in trashRows.ts, using a shared CommonTrashEntry shape and an
id-selector callback. Replace the trashRows mapping in DropboxTrashManager,
GoogleDriveTrashManager, KDriveTrashManager, MegaTrashManager,
PCloudTrashManager, and YandexTrashManager with this helper, passing each
provider’s existing id derivation unchanged.
In `@src/components/S3TrashManager.tsx`:
- Around line 155-251: Memoize the restore function used by s3Columns with
React’s useCallback, preserving its existing behavior and dependencies. Update
the restore definition near the component’s other callbacks so its reference
remains stable between renders, allowing the s3Columns useMemo dependency check
to work as intended.
In `@src/components/Trash/TrashTable.tsx`:
- Around line 118-125: The selection behavior described near onRowClick does not
match the implementation: only shift is forwarded, while plain clicks already
additively toggle selections. Update the header comment to remove the
Ctrl-specific claim, preserving the existing selectionAfterRowClick behavior and
modifier handling.
- Around line 140-152: Update the header rendering in TrashTable’s HEADERS map
to add aria-sort to each sortable th, deriving its value from the current sort
state for that column and using the unsorted value when inactive. Keep the
existing sort button behavior and label unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a32d223b-4119-4ac8-b2c9-4ac4c781fa81
📒 Files selected for processing (88)
src-tauri/src/dedupe/mod.rssrc-tauri/src/filesystem.rssrc-tauri/src/lib.rssrc-tauri/src/provider_commands.rssrc/App.tsxsrc/components/AeroSync/AeroSyncDialog.tsxsrc/components/AeroSync/CompareTabContent.tsxsrc/components/AeroSync/PlanTabContent.tsxsrc/components/AeroSync/SyncTabContent.tsxsrc/components/AeroSync/tabStateStore.test.tssrc/components/AeroSync/tabStateStore.tssrc/components/AzureTrashManager.tsxsrc/components/BoxTrashManager.tsxsrc/components/DropboxTrashManager.tsxsrc/components/DuplicateFinderDialog.tsxsrc/components/FileLuTrashManager.tsxsrc/components/GoogleDriveTrashManager.tsxsrc/components/InternxtTrashManager.tsxsrc/components/JottacloudTrashManager.tsxsrc/components/KDriveTrashManager.tsxsrc/components/KoofrTrashManager.tsxsrc/components/MegaTrashManager.tsxsrc/components/NextcloudTrashManager.tsxsrc/components/OneDriveTrashManager.tsxsrc/components/OpenDriveTrashManager.tsxsrc/components/PCloudTrashManager.tsxsrc/components/S3TrashManager.tsxsrc/components/SavedServers.tsxsrc/components/Trash/TrashTable.tsxsrc/components/Trash/trashRows.test.tssrc/components/Trash/trashRows.tssrc/components/YandexTrashManager.tsxsrc/components/ZohoTrashManager.tsxsrc/hooks/useScanProgress.test.tssrc/hooks/useScanProgress.tssrc/i18n/locales/bg.jsonsrc/i18n/locales/bn.jsonsrc/i18n/locales/ca.jsonsrc/i18n/locales/cs.jsonsrc/i18n/locales/cy.jsonsrc/i18n/locales/da.jsonsrc/i18n/locales/de.jsonsrc/i18n/locales/el.jsonsrc/i18n/locales/en.jsonsrc/i18n/locales/es.jsonsrc/i18n/locales/et.jsonsrc/i18n/locales/eu.jsonsrc/i18n/locales/fi.jsonsrc/i18n/locales/fr.jsonsrc/i18n/locales/gl.jsonsrc/i18n/locales/hi.jsonsrc/i18n/locales/hr.jsonsrc/i18n/locales/hu.jsonsrc/i18n/locales/hy.jsonsrc/i18n/locales/id.jsonsrc/i18n/locales/is.jsonsrc/i18n/locales/it.jsonsrc/i18n/locales/ja.jsonsrc/i18n/locales/ka.jsonsrc/i18n/locales/km.jsonsrc/i18n/locales/ko.jsonsrc/i18n/locales/lt.jsonsrc/i18n/locales/lv.jsonsrc/i18n/locales/mk.jsonsrc/i18n/locales/ms.jsonsrc/i18n/locales/nl.jsonsrc/i18n/locales/no.jsonsrc/i18n/locales/pl.jsonsrc/i18n/locales/pt.jsonsrc/i18n/locales/ro.jsonsrc/i18n/locales/ru.jsonsrc/i18n/locales/sk.jsonsrc/i18n/locales/sl.jsonsrc/i18n/locales/sr.jsonsrc/i18n/locales/sv.jsonsrc/i18n/locales/sw.jsonsrc/i18n/locales/th.jsonsrc/i18n/locales/tl.jsonsrc/i18n/locales/tr.jsonsrc/i18n/locales/uk.jsonsrc/i18n/locales/vi.jsonsrc/i18n/locales/zh.jsonsrc/types.tssrc/types/aerofile.tssrc/utils/cryptScope.test.tssrc/utils/cryptScope.tssrc/utils/nativeE2e.test.tssrc/utils/nativeE2e.ts
|
The Filed as #521 rather than patched here: the gate has no readiness budget of its own and inherits a timeout that exists for a different purpose, but making a gate more forgiving is not a change to slip into an unrelated PR. |
|
Correction to my note above, since it named the wrong cause and therefore the wrong fix. I wrote that the gate "has no readiness budget of its own" and inherits the app's 10-second splash timeout. It does have one: The timeline says the app started at 03:52:48 and the harness reported What is left is the cascade: after |
CodeRabbit Majors on #519, #505 and #504 that stayed unresolved after merge. Each change is pinned by a test that fails when the defect is reintroduced. 1. nativeE2e catalogue matcher (#519). The single-line `[^\n]*?` pattern only saw PROTOCOLS_FALLBACK; the live PROTOCOLS list puts type and badge on different lines. Entry-bounded extraction by successive type fields, plus a synthetic multiline pin that shows the old pattern matching nothing. 2. assert-private-a11y empty bus (#505). Already fail-closed (exit 7); extracted classify_names and a --selftest pin so "empty means pass" cannot return without a failing assertion. 3. fake-portal NetworkMonitor v3 (#505). GetStatus/CanReach already implemented; probe + selftest now exercise CanReach, and a source pin refuses a stand-in that advertises v3 without that surface. 4. speedtest scratch mkdir (#504). provider.mkdir was outside run_cancelable, so a stalled provider hung the test before upload. Cancellation now aborts with an explicit message; ordinary mkdir errors stay soft. Structure pin + hanging-future pin. Gates: vitest nativeE2e 9/9, python --selftest, fake-portal selftest 14/14, cargo test speedtest::tests 14/14, cargo fmt, clippy -D warnings --lib, tsc --noEmit. Co-Authored-By: Grok L2 <noreply@x.ai>
Add duplicates.fuzzyCutoffPlaceholder (synced to all 46 locales) for the hard-coded 'auto' in DuplicateFinderDialog. Clarify cy/sr sortWasted and vi/mk fuzzyCutoff labels per review on merged #519.
Ten wishlist comments @EhudKirsh left on 29 and 30 July in #347, plus three follow-ups he had left on threads that were already answered. Eight commits, one per theme.
Three of them were not the polish they were filed as. One is a bug that made an entire feature unusable, one is a number the UI has been printing that was never what it claimed to be, and one is a control that has never reached the code it configures.
AeroSync deleted the settings you were about to schedule (17832239)
He is not supposed to, and it was not a choice. The dialog renders
{activeTab === 'plan' && <PlanTabContent/>}, so leaving the tab unmounts it and React drops all fourteen of itsuseStatevalues — preset, direction, conflict policy, verify policy, canary, budget, streams, compression, EC — along with the Sync tab's paths, exclude list and dry-run.Mounting all three tabs at once would trade this for something worse: the Compare tab starts a recursive scan on mount. So the tabs still unmount and their settings live in a small store owned by the dialog, read back on remount and cleared on each fresh open. Two effects had to learn about it, both of which seed one control from another (speed mode → streams/compression, preset → EC) and therefore also fire on mount, which after a switch is a remount: they would have overwritten exactly the values just restored.
"dist" was a hash, not a distance (17833081)
Ehud asked to sort the non-identical duplicate groups from most to least similar. Writing that sort is what exposed why it could not have worked: the representative distance was
.max()of the signature values of the group's members, not of the hamming distances between them, on both the raster and the text arm. A pair clustered two bits apart could report a number in the billions. Anything that ordered by it — including the sort he asked for — would have ordered the groups by an arbitrary 64-bit integer. It is now the largest hamming distance from the group's first member, bounded by the threshold that admitted the group. Theotherarm was already computing a real TLSH diff.The rest of that comment is in too: each group carries its members' own signatures so a row can show why it is in the group (a "Show hashes" toggle keeps them out of the way by default), and the fuzzy cutoff is now the user's to set. It had never left the frontend —
find_duplicate_filestook no distance parameter and always passedNone, so the per-modality defaults were the only thing anyone could run. There is a Fuzzy cutoff field now, blank meaning those defaults, committed on blur or Enter rather than per keystroke since each commit re-runs the scan.The crypt toggle would not go away outside the Overlays Path (17831657 follow-up)
The badge asks
isWithinCryptScopewhich anchor the tab is bound to, and read it fromsession.cryptOverlay— the live vault, which locking clears. The scope then fell back to'', and to that predicate''does not mean "no anchor", it means "the whole remote is the anchor": every folder in the session read as inside, so the in-scope branch rendered everywhere and the jump-back button EF-02 put in its place could never appear while locked. The anchor now survives the lock the way the overlay kind already did.The rest
Scan progress (17831774, 17832864). Compare and Find Duplicates each answered a long scan with a bare spinner, the one display that cannot distinguish work from a hang. Compare needed no new backend: every compare walker has emitted
sync_scan_progresssince the recursive scan landed and nothing on the frontend ever listened. Those ticks now also carry directories and bytes. Find Duplicates had nothing to listen to, so the command emits its own — files, directories, bytes and depth during the walk, then a determinate fraction during the analysis pass, throttled to 120 ms so the reporting cannot slow the scan it reports on. Non-identical mode used to let the dedupe engine do its own walk; it now shares exact mode's, so the two cannot drift and both are visible.View Trash (17832100, 17832180, 17832268, 17832371). Four requests on one surface: sorting, a Type column, the Name header lining up with the names instead of reading as if it sat over the checkboxes, timestamps on one line, and rubber-band + Ctrl selection. The obstacle was that '🗑 View Trash' is not one dialog: seventeen provider components each carried their own copy of the same table, and
grep sortByacross all of them returned nothing. Sixty-eight edits and seventeen chances to diverge, or one shared table — this is the shared table, and all seventeen now render it, including the three awkward ones (Internxt read-only, FileLu's<li>list, S3's version/delete-marker view with its own Kind and Actions columns).Copy buttons in Find Duplicates (17832886). The
Copyglyph in that dialog was decorative. Each row now has a real copy button on the file name and on the folder path, plus buttons to open the file in its default app or reveal it in the file manager, so comparing two candidates no longer means reading a path off the screen with an OCR app.Encryption badge (17832803). Add Service has shown "E2E 128-bit"/"E2E 256-bit" on MEGA, Filen and Internxt for a long time; My Servers said it only in prose in the subtitle. Both now read one list and a test holds the Add Service catalogue to it. Not every MEGA protocol qualifies, and the test says so: MEGA S4 is a
protocol: 's3'profile where the server holds the keys, so badging it would be a false claim about who can read the files.TAB.DIGITAL (17831692 follow-up). The four Ehud still saw were in the VitePress data and config modules, which the 27 July markdown sweep never opened. Fixed in docs.aeroftp.app#4. The
tab.digitalURLs stay lowercase, including "Sign up at [tab.digital]", where the domain is the link text.Drime (17832505 follow-up). Split into #520, because it is a rename whose 47 locale files pushed this PR one file over CodeRabbit's review limit. Short version: he is right, the catalogue had it inside out, and the generated tables are regenerated rather than hand-edited.
Verification
tscclean,npm run buildok,vitest589/589 (39 new),cargo clippy --all-features --testsexit 0 with no warnings,cargo test --all-features --lib3381 passed,cargo fmt --checkclean,i18n:validate46/46 with 0 missing keys and 0 placeholders.No new i18n key anywhere in this PR. Every new label reuses one that is already translated in all 47 locales (
contextMenu.copyName,contextMenu.copyPath,contextMenu.open,aeroShare.inbox.revealFile,settings.columnType,connection.filenEncryptionTitle), and with the Drime rename split into #520 this PR does not touch the locale files at all.Two pins were checked against the defect rather than only against the fix: the crypt-scope test fails on the old resolution, and the dedupe distance test fails on the old
.max()expression.Merge order
Rebased onto
mainafter #515 landed. It sharessrc/App.tsxwith #506 (the crypt badge block), so it should land after #506 and after #516. #520 (the Drime rename, split out of this branch) is independent of both and of this one.Reported and specified throughout by @EhudKirsh.
None of these close their thread automatically: they stay open for the reporter to confirm.
Summary by CodeRabbit