Skip to content

feat: Ehud wishlist 2026-07-30 (#347) — AeroSync tab state, duplicate hashes, shared trash table - #519

Merged
axpnet merged 9 commits into
mainfrom
fix/ehud-wishlist-347-20260730
Jul 30, 2026
Merged

feat: Ehud wishlist 2026-07-30 (#347) — AeroSync tab state, duplicate hashes, shared trash table#519
axpnet merged 9 commits into
mainfrom
fix/ehud-wishlist-347-20260730

Conversation

@axpnet

@axpnet axpnet commented Jul 30, 2026

Copy link
Copy Markdown
Member

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)

I'm not sure how I'm supposed to be able to schedule a Plan with Update and other settings if they're deleted when switching to the Sync tab.

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 its useState values — 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. The other arm 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_files took no distance parameter and always passed None, 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)

toggling off the '🔒 RCLONE CRYPT' or '🔒 AEROCRYPT' toggle switch so it looks grey, and then going to the Remote Path directory, the grey toggle switch is still showing.

The badge asks isWithinCryptScope which anchor the tab is bound to, and read it from session.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_progress since 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 sortBy across 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 Copy glyph 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.digital URLs 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

tsc clean, npm run build ok, vitest 589/589 (39 new), cargo clippy --all-features --tests exit 0 with no warnings, cargo test --all-features --lib 3381 passed, cargo fmt --check clean, i18n:validate 46/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 main after #515 landed. It shares src/App.tsx with #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

  • New Features
    • Added live duplicate-scan progress (elapsed time + file/dir/byte counters), per-file fuzzy signatures, and richer duplicate results (sorting, optional hash display, and quick actions).
    • Added fuzzy cutoff controls and improved directory-compare progress metrics.
    • Standardized trash tables across providers with shared sorting/selection; added native E2E encryption badges for saved services.
    • Preserved Plan/Sync tab state and improved crypt-scope handling for locked vs unlocked behavior.
  • Bug Fixes
    • Fixed fuzzy similarity “distance” to use true Hamming-distance and ensured the cutoff/threshold behavior is honored.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The 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)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and clearly describes the main themes of the change set: AeroSync state persistence, duplicate scanning updates, and a shared trash table.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ehud-wishlist-347-20260730

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.rs

ast-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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@snyk-io

snyk-io Bot commented Jul 30, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Emit 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, and bytes_found before 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 value

Expose the progress bar to assistive tech.

The bar is a bare div; add role="progressbar" with aria-valuenow/aria-valuemin/aria-valuemax so 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

formatElapsed duplicates the helper in src/hooks/useScanProgress.ts.

The same formatter (with tests) lands in src/hooks/useScanProgress.ts in this PR stack and is consumed by src/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 value

Exact-mode files_total is 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 rendering files_processed / files_total will stall well below 100% (the fuzzy arm is consistent because it ticks every path). The GUI path avoids this by computing its own hash_total in filesystem.rs, so this only bites other callers of find_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 lift

Offload duplicate scanning from the async command worker.

find_duplicate_files now performs the directory walk, synchronous file reads, BLAKE3 hashing, and, in non-identical mode, pHash/SimHash/TLSH signing and clustering inside a Tauri async command. Keep the event loop responsive by moving the scanning/computation work to tauri::async_runtime::spawn_blocking (returning AppHandle progress/callbacks to emit events). Applies here and in the non-identical analysis branch that calls find_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 win

Extract a shared TrashRow mapper instead of repeating the same items.map(...) in 9 files. Every provider trash manager duplicates the identical normalization shape (id/name/isDir/size/deletedAt), differing only in how id is derived; a single helper in trashRows.ts (e.g. toTrashRows(items, idFn)) would remove the drift risk when TrashRow gains/changes fields.

  • src/components/BoxTrashManager.tsx#L128-L139: replace with toTrashRows(items, getItemId), wrapped in useMemo(..., [items]).
  • src/components/InternxtTrashManager.tsx#L61-L72: replace with toTrashRows(items, item => item.path).
  • src/components/JottacloudTrashManager.tsx#L118-L129: replace with toTrashRows(items, item => item.path).
  • src/components/MegaTrashManager.tsx#L118-L129: replace with toTrashRows(items, item => item.name).
  • src/components/OneDriveTrashManager.tsx#L123-L134: replace with toTrashRows(items, getItemId).
  • src/components/OpenDriveTrashManager.tsx#L141-L152: replace with toTrashRows(items, itemId).
  • src/components/PCloudTrashManager.tsx#L134-L145: replace with toTrashRows(items, item => item.path).
  • src/components/YandexTrashManager.tsx#L129-L140: replace with toTrashRows(items, item => item.path).
  • src/components/ZohoTrashManager.tsx#L124-L135: replace with toTrashRows(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 win

Consider covering deletedAtMs precedence.

trashRowDeletedMs prefers deletedAtMs over deletedAt, and NextcloudTrashManager depends on it exclusively, but no fixture here sets deletedAtMs. A row with deletedAtMs plus a conflicting/absent deletedAt would 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 win

Expose sort state to assistive tech.

The header button's aria-label just 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). Add aria-sort to 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

s3Columns memo never memoizes.

restore is redefined on every render (Line 84), so the dependency comparison always fails and this useMemo recomputes each time. Wrap restore in useCallback (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

📥 Commits

Reviewing files that changed from the base of the PR and between c5369ee and a793e7d.

📒 Files selected for processing (41)
  • src-tauri/src/dedupe/mod.rs
  • src-tauri/src/filesystem.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/provider_commands.rs
  • src/App.tsx
  • src/components/AeroSync/AeroSyncDialog.tsx
  • src/components/AeroSync/CompareTabContent.tsx
  • src/components/AeroSync/PlanTabContent.tsx
  • src/components/AeroSync/SyncTabContent.tsx
  • src/components/AeroSync/tabStateStore.test.ts
  • src/components/AeroSync/tabStateStore.ts
  • src/components/AzureTrashManager.tsx
  • src/components/BoxTrashManager.tsx
  • src/components/DropboxTrashManager.tsx
  • src/components/DuplicateFinderDialog.tsx
  • src/components/FileLuTrashManager.tsx
  • src/components/GoogleDriveTrashManager.tsx
  • src/components/InternxtTrashManager.tsx
  • src/components/JottacloudTrashManager.tsx
  • src/components/KDriveTrashManager.tsx
  • src/components/KoofrTrashManager.tsx
  • src/components/MegaTrashManager.tsx
  • src/components/NextcloudTrashManager.tsx
  • src/components/OneDriveTrashManager.tsx
  • src/components/OpenDriveTrashManager.tsx
  • src/components/PCloudTrashManager.tsx
  • src/components/S3TrashManager.tsx
  • src/components/SavedServers.tsx
  • src/components/Trash/TrashTable.tsx
  • src/components/Trash/trashRows.test.ts
  • src/components/Trash/trashRows.ts
  • src/components/YandexTrashManager.tsx
  • src/components/ZohoTrashManager.tsx
  • src/hooks/useScanProgress.test.ts
  • src/hooks/useScanProgress.ts
  • src/types.ts
  • src/types/aerofile.ts
  • src/utils/cryptScope.test.ts
  • src/utils/cryptScope.ts
  • src/utils/nativeE2e.test.ts
  • src/utils/nativeE2e.ts

Comment thread src-tauri/src/filesystem.rs
Comment thread src-tauri/src/filesystem.rs
Comment thread src-tauri/src/provider_commands.rs Outdated
Comment thread src-tauri/src/provider_commands.rs Outdated
Comment thread src/components/AeroSync/AeroSyncDialog.tsx Outdated
Comment thread src/components/DuplicateFinderDialog.tsx
Comment thread src/components/FileLuTrashManager.tsx
Comment thread src/components/FileLuTrashManager.tsx
Comment thread src/components/NextcloudTrashManager.tsx
Comment thread src/components/Trash/TrashTable.tsx
@axpnet

axpnet commented Jul 30, 2026

Copy link
Copy Markdown
Member Author

All ten addressed in 12be1292b, none dismissed. Two of them were defects in this PR's own new code, one is a performance mistake in it, one predates it, and the rest are correctness details in the new UI.

force never passed true — right, and the consequence is worse than a missing frame: the throttle could swallow the final tick, so the bar stayed short of the total for as long as the dialog stayed open. Both modes now force a tick when the pass ends.

Fuzzy path has no I/O budget — right, and it predates this PR: the engine buffered every candidate's full contents in a Vec before computing any signature. It now reads and signs one file at a time, dropping the bytes as soon as the signature is out, under the same 2 GB budget exact mode uses. Reading one large file at a time is bounded; holding a directory of them was not.

Always-zero clone-pool dirs_found — right, and the fix is to say nothing rather than to find a number: that branch returns a flat file list, so there is no directory count to report on that path. The field is omitted and the frontend reducer keeps the last value it saw, which is the behaviour its test already pins.

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 (duplicates.sortWasted, sortSimilarity, showHashes, fuzzyCutoff, fuzzyCutoffHint), as one-line insertions so each catalogue keeps its formatting. The <label> with nothing to point at is fixed by passing the text through the Checkbox's own label prop, which also gives it the hit area the cursor was promising.

FileLu null file_code — right, and it is three bugs in one: duplicate React keys, one selectable row standing for many, and a per-row action invoked with an empty code. Filtered out, matching the two .filter(Boolean) call sites already in that file.

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 deleted_at === 0 — right: displayed as an em dash, sorted as 1970. Mapped to null so the sort agrees with the display.

size: null renders 0 B — right, and sortTrashRows already disagreed with the cell. Null is now a dash for rows that have no size, not only for folders.

Re-verified after the changes: tsc clean, vitest 589/589, i18n:validate 47/47 with 0 missing keys and 0 placeholders, cargo clippy --all-features --tests with no warnings, cargo fmt --check clean, dedupe tests green.

axpnet and others added 9 commits July 30, 2026 05:15
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>
@axpnet
axpnet force-pushed the fix/ehud-wishlist-347-20260730 branch from 12be129 to d02bcda Compare July 30, 2026 03:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Align the Drime free-tier strings to 20 GB.

drimeDesc currently says “21 GB free”, while drimeTooltip and 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 win

Do 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 -N suffix. 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 win

Translate 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 win

Six providers repeat the same TrashRow mapping boilerplate. Dropbox, GoogleDrive, KDrive, Mega, PCloud, and Yandex all build trashRows with the identical { name, isDir: item.is_dir, size: item.size, deletedAt: item.modified } shape, differing only in how id is derived. A shared helper (e.g. exported from trashRows.ts) parameterized by an id-selector would remove this duplication and keep any future TrashRow field additions single-sourced.

  • src/components/DropboxTrashManager.tsx#L167-177: replace with a call to a shared toTrashRows(items, getItemId) helper.
  • src/components/GoogleDriveTrashManager.tsx#L124-134: replace with the same shared helper, passing getItemId.
  • src/components/KDriveTrashManager.tsx#L137-147: replace with the same shared helper, passing item => item.path.
  • src/components/MegaTrashManager.tsx#L120-130: replace with the same shared helper, passing item => item.name.
  • src/components/PCloudTrashManager.tsx#L136-146: replace with the same shared helper, passing item => item.path.
  • src/components/YandexTrashManager.tsx#L131-141: replace with the same shared helper, passing item => 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

s3Columns useMemo never actually memoizes — restore is a fresh function every render.

restore (line 84) is a plain closure recreated on every render, not memoized with useCallback. Since it's a dependency of the s3Columns useMemo (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 dep restore is 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 value

Header comment overstates Ctrl handling.

Lines 15-16 say "Ctrl adds without losing what is already selected", but onRowClick forwards only shift; 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 win

Expose sort state to assistive tech with aria-sort.

The direction is conveyed only by the chevron glyph, and the aria-label just repeats the visible button text. aria-sort on 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

📥 Commits

Reviewing files that changed from the base of the PR and between a793e7d and d02bcda.

📒 Files selected for processing (88)
  • src-tauri/src/dedupe/mod.rs
  • src-tauri/src/filesystem.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/provider_commands.rs
  • src/App.tsx
  • src/components/AeroSync/AeroSyncDialog.tsx
  • src/components/AeroSync/CompareTabContent.tsx
  • src/components/AeroSync/PlanTabContent.tsx
  • src/components/AeroSync/SyncTabContent.tsx
  • src/components/AeroSync/tabStateStore.test.ts
  • src/components/AeroSync/tabStateStore.ts
  • src/components/AzureTrashManager.tsx
  • src/components/BoxTrashManager.tsx
  • src/components/DropboxTrashManager.tsx
  • src/components/DuplicateFinderDialog.tsx
  • src/components/FileLuTrashManager.tsx
  • src/components/GoogleDriveTrashManager.tsx
  • src/components/InternxtTrashManager.tsx
  • src/components/JottacloudTrashManager.tsx
  • src/components/KDriveTrashManager.tsx
  • src/components/KoofrTrashManager.tsx
  • src/components/MegaTrashManager.tsx
  • src/components/NextcloudTrashManager.tsx
  • src/components/OneDriveTrashManager.tsx
  • src/components/OpenDriveTrashManager.tsx
  • src/components/PCloudTrashManager.tsx
  • src/components/S3TrashManager.tsx
  • src/components/SavedServers.tsx
  • src/components/Trash/TrashTable.tsx
  • src/components/Trash/trashRows.test.ts
  • src/components/Trash/trashRows.ts
  • src/components/YandexTrashManager.tsx
  • src/components/ZohoTrashManager.tsx
  • src/hooks/useScanProgress.test.ts
  • src/hooks/useScanProgress.ts
  • src/i18n/locales/bg.json
  • src/i18n/locales/bn.json
  • src/i18n/locales/ca.json
  • src/i18n/locales/cs.json
  • src/i18n/locales/cy.json
  • src/i18n/locales/da.json
  • src/i18n/locales/de.json
  • src/i18n/locales/el.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/et.json
  • src/i18n/locales/eu.json
  • src/i18n/locales/fi.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/gl.json
  • src/i18n/locales/hi.json
  • src/i18n/locales/hr.json
  • src/i18n/locales/hu.json
  • src/i18n/locales/hy.json
  • src/i18n/locales/id.json
  • src/i18n/locales/is.json
  • src/i18n/locales/it.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ka.json
  • src/i18n/locales/km.json
  • src/i18n/locales/ko.json
  • src/i18n/locales/lt.json
  • src/i18n/locales/lv.json
  • src/i18n/locales/mk.json
  • src/i18n/locales/ms.json
  • src/i18n/locales/nl.json
  • src/i18n/locales/no.json
  • src/i18n/locales/pl.json
  • src/i18n/locales/pt.json
  • src/i18n/locales/ro.json
  • src/i18n/locales/ru.json
  • src/i18n/locales/sk.json
  • src/i18n/locales/sl.json
  • src/i18n/locales/sr.json
  • src/i18n/locales/sv.json
  • src/i18n/locales/sw.json
  • src/i18n/locales/th.json
  • src/i18n/locales/tl.json
  • src/i18n/locales/tr.json
  • src/i18n/locales/uk.json
  • src/i18n/locales/vi.json
  • src/i18n/locales/zh.json
  • src/types.ts
  • src/types/aerofile.ts
  • src/utils/cryptScope.test.ts
  • src/utils/cryptScope.ts
  • src/utils/nativeE2e.test.ts
  • src/utils/nativeE2e.ts

Comment thread src-tauri/src/dedupe/mod.rs
Comment thread src-tauri/src/lib.rs
Comment thread src/components/DuplicateFinderDialog.tsx
Comment thread src/components/FileLuTrashManager.tsx
Comment thread src/i18n/locales/cy.json
Comment thread src/i18n/locales/mk.json
Comment thread src/i18n/locales/ru.json
Comment thread src/i18n/locales/sr.json
Comment thread src/i18n/locales/vi.json
Comment thread src/utils/nativeE2e.test.ts
@axpnet

axpnet commented Jul 30, 2026

Copy link
Copy Markdown
Member Author

The File chooser is portal-backed red on this branch was timing, not code: a re-run of the same commit (d02bcdaf6) passed. The app hit its own 10-second splash safety timeout before the frontend signalled app_ready, so the harness never drove the UI and eleven downstream assertions failed after it; the frontend server had answered 200, both NetworkMonitor and ProxyResolver were asked and answered, and scenarios 2 and 3 started the app fine in the same job. cancel.log reaches the same conclusion on its own: "the app never reported app_ready, so the UI was never driven. This is an environment failure, not a chooser failure."

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.

@axpnet
axpnet merged commit 8952794 into main Jul 30, 2026
17 of 18 checks passed
@axpnet

axpnet commented Jul 30, 2026

Copy link
Copy Markdown
Member Author

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: recon-session.sh waits up to RECON_WAIT_SECONDS, default 45 seconds, for app_ready, and already fails closed with a single named precondition error.

The timeline says the app started at 03:52:48 and the harness reported app_ready seen: 0 at 03:53:39, so it used that full budget. The app did not miss a 10-second deadline: it never rendered at all in 45 seconds, with its own frontend server answering 200, which per the harness's own comment means WebKit did not render a bundle that was served fine. Still environmental, still did not reproduce on the re-run, but the fix is not a longer wait.

What is left is the cascade: after run_case returns non-zero, portal-chooser-test.sh keeps evaluating ten more assertions against evidence the failed session could not produce, so a rendering hiccup reads as "the chooser did NOT go out of process". #521 is updated with the corrected diagnosis and a way to reproduce the shape deliberately (RECON_WAIT_SECONDS=1) instead of waiting for a bad runner.

axpnet added a commit that referenced this pull request Jul 30, 2026
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>
axpnet added a commit that referenced this pull request Jul 30, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant