Skip to content

fix: show move/trash progress as a count-up toward the batch total#29

Open
f4nu wants to merge 4 commits into
baronunread:mainfrom
f4nu:show-move-batch-total
Open

fix: show move/trash progress as a count-up toward the batch total#29
f4nu wants to merge 4 commits into
baronunread:mainfrom
f4nu:show-move-batch-total

Conversation

@f4nu

@f4nu f4nu commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

The transfer widget's move/trash header counted only the items currently in flight (movingMoves.length). Because bulk moves and trashes are throttled to BULK_OP_CONCURRENCY (3) items at a time, that count sat pinned at 3 for almost the whole operation, then ticked down 3 → 2 → 1 as the queue drained — a countdown that read as the opposite of progress, and gave no sense of how big the batch actually was.

This PR makes the header a true progress indicator that counts up toward the batch total:

Moved 0 of 76 items…  →  1 of 76  →  …  →  76 items moved

How it works

  • Adds an optional batchTotal to MoveProgress, set once per batch at the call sites in RemoteBrowser.tsx (handleMove / handleBulkDeleteToTrash), where the selection size is already known before the concurrency-limited loop starts. Threaded through runTracked / browser.move / browser.delete.
  • widgetTitle() shows Moved {completed} of {batchTotal} items… for real batches (batchTotal > 1), counting only items that have actually finished, not the in-flight ones.
  • Single-item operations (a one-file drag-move, Trash dialog actions) are unaffected — they keep the existing "Moving 1 item…" text.

Also addressed (CodeRabbit review)

  • browser.delete file path bypassed runTracked — bulk-deleting files (vs folders) to Trash silently dropped batchTotal and never appeared in the widget. Both branches now run through runTracked; a single file reports totalItems: 1 since s3MoveFileToTrash has no sub-object progress of its own.
  • handleBulkDeleteToTrash aborted the whole batch on the first error and rolled back every item, including ones already trashed server-side — a permanent UI/server desync. Now matches handleMove's per-item catch pattern, so partial failures roll back only what didn't succeed and refreshSilently() always runs.

Test plan

  • bun run typecheck passes
  • bun test tests/unit — full suite (305 tests) passes
  • Added a test proving the header counts finished items up (1 done + 1 in-flight ⇒ "Moved 1 of 5 items…")

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Bulk move and delete operations now display progress toward the total number of selected items.
    • Single-file deletions accurately report one item in completion progress.
    • Progress updates remain available during individual operations.
  • Bug Fixes

    • Improved bulk-delete error handling, including per-item failure notifications and rollback behavior.
  • Tests

    • Added coverage verifying bulk progress displays, such as “Moved 1 of 5 items…”.

The header pinned its count to BULK_OP_CONCURRENCY (3) items in flight
for the whole batch, so moving/trashing dozens of items looked stuck at
"3" the entire time even though the queue was draining correctly behind
the scenes. Threads the real batch size through to MoveProgress so the
header can show "3 of 76 items" instead.
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Bulk move and trash operations now propagate batch totals through tracked progress. The transfer widget displays completed item counts against the full batch size, while single-file trash operations seed item progress for accurate completion reporting.

Changes

Batch progress reporting

Layer / File(s) Summary
Tracked operation batch totals
src/ui/services.ts, src/services/appServices.ts
Move progress and browser service APIs accept optional batch totals, which runTracked includes in initial progress events; single-file trash seeds one completed item in tracked progress.
Bulk operation propagation
src/ui/RemoteBrowser.tsx
Bulk move and trash handlers calculate selection sizes and pass them to each browser operation; bulk trash failures are handled per item before rollback and refresh.
Aggregate progress display and validation
src/ui/TransferWidget.tsx, tests/unit/ui/MoveProgress.test.tsx
The widget renders “N of M” titles for active batches, with a test covering concurrent moves and stalled progress.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: baronunread

Sequence Diagram(s)

sequenceDiagram
  participant RemoteBrowser
  participant BrowserService
  participant runTracked
  participant TransferWidget
  RemoteBrowser->>BrowserService: start bulk move or trash with batchTotal
  BrowserService->>runTracked: forward batchTotal
  runTracked->>TransferWidget: publish MoveProgress
  TransferWidget-->>RemoteBrowser: display completed count of batch total
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main user-visible change: move/trash progress now counts up toward the batch total.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/services/appServices.ts (1)

452-467: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Track file deletions to display bulk trash progress in the widget.

Currently, browser.delete bypasses runTracked for files. This means that if a user bulk-deletes files to the Trash, batchTotal is discarded and the operation won't appear in the transfer widget. In contrast, browser.move correctly wraps both files and folders in runTracked.

Wrap the file deletion path in runTracked so that the start and completion events are captured, and the batch count is properly displayed in the UI.

🛠️ Proposed fix to unify tracking
     delete: async (connectionId: string, key: string, batchTotal?: number): Promise<void> => {
       const { client, conn } = await this.getClient(connectionId);
       const deletedAtMs = Date.now();
-      if (isFolderKey(key)) {
-        await this.runTracked(
-          connectionId,
-          key,
-          key,
-          "trash",
-          (emit) => s3MoveFolderToTrash(client, conn.bucket, key, deletedAtMs, emit),
-          batchTotal,
-        );
-      } else {
-        await s3MoveFileToTrash(client, conn.bucket, key, deletedAtMs);
-      }
+      await this.runTracked(
+        connectionId,
+        key,
+        key,
+        "trash",
+        async (emit) => {
+          if (isFolderKey(key)) {
+            await s3MoveFolderToTrash(client, conn.bucket, key, deletedAtMs, emit);
+          } else {
+            await s3MoveFileToTrash(client, conn.bucket, key, deletedAtMs);
+          }
+        },
+        batchTotal,
+      );
     },
🤖 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/services/appServices.ts` around lines 452 - 467, Update the file branch
of browser.delete to invoke runTracked, matching the folder branch, with
connectionId, key, the "trash" operation, and a callback that calls
s3MoveFileToTrash with the existing client, bucket, key, and deletedAtMs. Pass
batchTotal so bulk file deletions emit start and completion events and retain
their batch count.
src/ui/RemoteBrowser.tsx (1)

577-591: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Handle bulk trash errors per-item to prevent UI desync.

Because mapWithConcurrency throws on the first error, the current try-catch block aborts the entire batch midway and calls rollback(). However, because it misses a refreshSilently() in the catch block, any items that were successfully moved to the Trash before the failure occurred will be wrongly restored to the UI, causing a permanent state desync and 404s on subsequent interactions.

Refactoring this to match the handleMove pattern (catching errors per-item and evaluating anyFailed at the end) guarantees that refreshSilently() is always called, which resyncs the UI with the true state on the server.

🔄 Proposed fix to synchronize state and unify error handling
-    try {
-      const batchTotal = deleted.length;
-      await mapWithConcurrency(deleted, BULK_OP_CONCURRENCY, (entry) =>
-        services.browser.delete(connectionId, entry.key, batchTotal),
-      );
-      for (const entry of deleted) invalidateForKey(connectionId, entry.key);
-      await refreshSilently();
-    } catch (err) {
-      rollback();
-      toasts.add({
-        variant: "error",
-        title: "Couldn't move to Trash",
-        description: err instanceof Error ? err.message : "Something went wrong.",
-      });
-    }
+    let anyFailed = false;
+    const batchTotal = deleted.length;
+    void mapWithConcurrency(deleted, BULK_OP_CONCURRENCY, async (entry) => {
+      try {
+        await services.browser.delete(connectionId, entry.key, batchTotal);
+      } catch (err) {
+        anyFailed = true;
+        toasts.add({
+          variant: "error",
+          title: "Couldn't move to Trash",
+          description: err instanceof Error ? err.message : "Something went wrong.",
+        });
+      }
+    }).then(() => {
+      if (anyFailed) rollback();
+      for (const entry of deleted) invalidateForKey(connectionId, entry.key);
+      void refreshSilently();
+    });
🤖 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/ui/RemoteBrowser.tsx` around lines 577 - 591, Update the bulk-delete flow
around mapWithConcurrency to catch errors per entry, track whether any deletion
failed, and allow all items to be attempted instead of aborting on the first
error. Match the existing handleMove error-handling pattern, rolling back only
when appropriate, showing the error notification for failures, and always
calling refreshSilently() so the UI reflects the server state.
🧹 Nitpick comments (1)
src/ui/TransferWidget.tsx (1)

387-389: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify unreachable pluralization check.

Since n is guaranteed to be at least 1 (enforced by the p.movingMoves.length > 0 guard block), the condition batchTotal > n ensures that batchTotal is at least 2. Thus, batchTotal === 1 is strictly impossible in the true branch, and we can safely default to the plural "items".

🧹 Proposed refactor to simplify the string
     return batchTotal > n
-      ? `${moveVerb} ${n} of ${batchTotal} item${batchTotal === 1 ? "" : "s"}…`
+      ? `${moveVerb} ${n} of ${batchTotal} items…`
       : `${moveVerb} ${n} item${n === 1 ? "" : "s"}…`;
🤖 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/ui/TransferWidget.tsx` around lines 387 - 389, Update the true branch of
the move-label expression in the TransferWidget formatting logic to use the
plural “items” directly, removing the unreachable batchTotal === 1 check.
Preserve the existing singular/plural handling in the false branch.
🤖 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.

Outside diff comments:
In `@src/services/appServices.ts`:
- Around line 452-467: Update the file branch of browser.delete to invoke
runTracked, matching the folder branch, with connectionId, key, the "trash"
operation, and a callback that calls s3MoveFileToTrash with the existing client,
bucket, key, and deletedAtMs. Pass batchTotal so bulk file deletions emit start
and completion events and retain their batch count.

In `@src/ui/RemoteBrowser.tsx`:
- Around line 577-591: Update the bulk-delete flow around mapWithConcurrency to
catch errors per entry, track whether any deletion failed, and allow all items
to be attempted instead of aborting on the first error. Match the existing
handleMove error-handling pattern, rolling back only when appropriate, showing
the error notification for failures, and always calling refreshSilently() so the
UI reflects the server state.

---

Nitpick comments:
In `@src/ui/TransferWidget.tsx`:
- Around line 387-389: Update the true branch of the move-label expression in
the TransferWidget formatting logic to use the plural “items” directly, removing
the unreachable batchTotal === 1 check. Preserve the existing singular/plural
handling in the false branch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d23adef0-8cb0-4dab-ac80-128f831317c2

📥 Commits

Reviewing files that changed from the base of the PR and between 69985cb and 2afec16.

📒 Files selected for processing (5)
  • src/services/appServices.ts
  • src/ui/RemoteBrowser.tsx
  • src/ui/TransferWidget.tsx
  • src/ui/services.ts
  • tests/unit/ui/MoveProgress.test.tsx

f4nu added 2 commits July 20, 2026 22:33
- browser.delete's file path bypassed runTracked entirely, so bulk-
  deleting files silently dropped batchTotal and never showed up in
  the transfer widget. Wrap both branches in runTracked; single files
  now report totalItems: 1 since s3MoveFileToTrash has no sub-object
  progress of its own to emit.
- handleBulkDeleteToTrash aborted the whole batch on the first error
  and rolled back every item, including ones already trashed
  successfully server-side — a permanent UI/server desync. Match
  handleMove's per-item catch pattern so partial failures roll back
  only what didn't succeed and refreshSilently always runs.
- Drop an unreachable batchTotal === 1 branch in widgetTitle: the
  surrounding guard already ensures batchTotal >= 2 there.
The previous "N of M" showed the in-flight count, which sits pinned at
BULK_OP_CONCURRENCY then ticks 3 → 2 → 1 as the queue drains — a
countdown that reads as the opposite of progress. Count the items that
have actually finished instead, so the header climbs "Moved 0 of 76" →
"1 of 76" → … toward the total.
@f4nu f4nu changed the title fix: show batch total in the transfer widget's move/trash header fix: show move/trash progress as a count-up toward the batch total Jul 20, 2026
@baronunread
baronunread self-requested a review July 20, 2026 21:11
# Conflicts:
#	tests/unit/ui/MoveProgress.test.tsx

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/ui/TransferWidget.tsx (1)

528-537: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Ensure failed moves are cleared by the broom button.

hasProcessed evaluates to true for ANY settled move, including "failed". However, if clearCompleted() only clears moves with "completed" status (as its name implies), any "failed" moves will remain in the list. This leaves the broom button permanently visible and stuck, as clicking it won't dismiss the failed item.

Explicitly dismissing all settled moves guarantees both completed and failed items are cleared, mirroring the behavior of clearAll(). (Note: You can then safely remove clearCompleted from the useMoveProgress destructuring on line 453).

🧹 Proposed fix
-  const hasProcessed = settledTransfers.length > 0 || visibleMoves.some((m) => m.status !== "moving");
+  const settledMoves = visibleMoves.filter((m) => m.status !== "moving");
+  const hasProcessed = settledTransfers.length > 0 || settledMoves.length > 0;
   function clearProcessed() {
     setDismissed((prev) => {
       const next = new Set(prev);
       for (const t of settledTransfers) next.add(t.id);
       return next;
     });
     for (const t of settledTransfers) void services.engine.dismiss(t.id);
-    clearCompleted();
+    for (const m of settledMoves) dismissMove(m.moveId);
   }
🤖 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/ui/TransferWidget.tsx` around lines 528 - 537, Update clearProcessed in
TransferWidget to dismiss every settled transfer, including failed moves, rather
than relying on clearCompleted’s completed-only behavior. Preserve the existing
dismissed-state updates and engine dismissal loop, remove the clearCompleted
invocation, and remove clearCompleted from the useMoveProgress destructuring.
🤖 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/ui/TransferWidget.tsx`:
- Line 390: Add a batchId field to the MoveProgress interface, generate and pass
a unique batch ID from each bulk-action call site, and update the count near
completedMoves.length to include only completed moves matching the active
batchId. Preserve existing progress behavior while preventing retained results
from earlier batches from affecting the current count.
- Around line 388-393: In the batch progress branch of the transfer widget,
replace the hardcoded “Moved” text with the existing completedVerb value so
progress messages use the localized verb for each operation kind, including
purge. Keep the batch counts and surrounding formatting unchanged.

---

Outside diff comments:
In `@src/ui/TransferWidget.tsx`:
- Around line 528-537: Update clearProcessed in TransferWidget to dismiss every
settled transfer, including failed moves, rather than relying on
clearCompleted’s completed-only behavior. Preserve the existing dismissed-state
updates and engine dismissal loop, remove the clearCompleted invocation, and
remove clearCompleted from the useMoveProgress destructuring.
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

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

Run ID: ce92f04e-bbb0-466e-9164-59f6ebb60aca

📥 Commits

Reviewing files that changed from the base of the PR and between 2afec16 and cebfc62.

📒 Files selected for processing (4)
  • src/services/appServices.ts
  • src/ui/RemoteBrowser.tsx
  • src/ui/TransferWidget.tsx
  • tests/unit/ui/MoveProgress.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/ui/RemoteBrowser.tsx

Comment thread src/ui/TransferWidget.tsx
Comment on lines +388 to 393
const batchTotal = Math.max(0, ...p.movingMoves.map((m) => m.batchTotal ?? 0));
if (batchTotal > 1) {
const done = p.completedMoves.length;
return `Moved ${done} of ${batchTotal} items…`;
}
return `${moveVerb} ${n} item${n === 1 ? "" : "s"}…`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a dynamic verb based on the operation kind.

The hardcoded "Moved" string applies to all operations, including "purge" (Empty Trash). This results in confusing progress text like "Moved 1 of 5 items…" during a bulk deletion.

Use completedVerb (which is already imported and used on line 406) to properly localize the verb to the specific operation kind.

💡 Proposed fix
-    const batchTotal = Math.max(0, ...p.movingMoves.map((m) => m.batchTotal ?? 0));
-    if (batchTotal > 1) {
-      const done = p.completedMoves.length;
-      return `Moved ${done} of ${batchTotal} items…`;
-    }
+    const batchTotal = Math.max(0, ...p.movingMoves.map((m) => m.batchTotal ?? 0));
+    if (batchTotal > 1) {
+      const done = p.completedMoves.length;
+      const verbed = kinds.size === 1 ? completedVerb(p.movingMoves[0].kind) : "moved";
+      const capitalizedVerbed = verbed.charAt(0).toUpperCase() + verbed.slice(1);
+      return `${capitalizedVerbed} ${done} of ${batchTotal} items…`;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const batchTotal = Math.max(0, ...p.movingMoves.map((m) => m.batchTotal ?? 0));
if (batchTotal > 1) {
const done = p.completedMoves.length;
return `Moved ${done} of ${batchTotal} items…`;
}
return `${moveVerb} ${n} item${n === 1 ? "" : "s"}…`;
const batchTotal = Math.max(0, ...p.movingMoves.map((m) => m.batchTotal ?? 0));
if (batchTotal > 1) {
const done = p.completedMoves.length;
const verbed = kinds.size === 1 ? completedVerb(p.movingMoves[0].kind) : "moved";
const capitalizedVerbed = verbed.charAt(0).toUpperCase() + verbed.slice(1);
return `${capitalizedVerbed} ${done} of ${batchTotal} items…`;
}
return `${moveVerb} ${n} item${n === 1 ? "" : "s"}…`;
🤖 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/ui/TransferWidget.tsx` around lines 388 - 393, In the batch progress
branch of the transfer widget, replace the hardcoded “Moved” text with the
existing completedVerb value so progress messages use the localized verb for
each operation kind, including purge. Keep the batch counts and surrounding
formatting unchanged.

Comment thread src/ui/TransferWidget.tsx
// (the larger) — add a batchId if that ever actually comes up.
const batchTotal = Math.max(0, ...p.movingMoves.map((m) => m.batchTotal ?? 0));
if (batchTotal > 1) {
const done = p.completedMoves.length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Incorrect count of completed items across sequential batches.

p.completedMoves contains all completed moves currently retained in the widget. If a user performs a bulk move, does not manually click the "Clear finished" broom, and then performs a second bulk move, done will incorrectly include the first batch's completed items. This results in an artificially inflated count (e.g., displaying "Moved 10 of 5 items…") while the second batch processes.

Since there is no way to distinguish which batch a completed move belongs to, the reliable fix is to add a batchId property to the MoveProgress interface, populate it in the bulk action call sites (e.g., using crypto.randomUUID()), and filter p.completedMoves here to only count items matching the active batchId.

🤖 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/ui/TransferWidget.tsx` at line 390, Add a batchId field to the
MoveProgress interface, generate and pass a unique batch ID from each
bulk-action call site, and update the count near completedMoves.length to
include only completed moves matching the active batchId. Preserve existing
progress behavior while preventing retained results from earlier batches from
affecting the current count.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

An unexpected error occurred while generating fixes: Not Found - https://docs.github.com/rest/git/refs#get-a-reference

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