Skip to content

feat: add multi-select and drag-to-move for file tree and changes panel#490

Merged
zeval merged 25 commits intomainfrom
feature/multi-entity-selecti-eyb
Apr 11, 2026
Merged

feat: add multi-select and drag-to-move for file tree and changes panel#490
zeval merged 25 commits intomainfrom
feature/multi-entity-selecti-eyb

Conversation

@zeval
Copy link
Copy Markdown
Contributor

@zeval zeval commented Mar 29, 2026

Summary

  • Add Shift-click range and Ctrl/Cmd-click toggle multi-select to both the file tree and staged/unstaged changes panel
  • Bulk actions: delete selected files (file tree), stage/unstage/discard selected files (changes panel)
  • Drag-and-drop files onto directories to move them (uses existing rename API)
  • Keyboard shortcuts: Escape clears selection, Ctrl+A selects all visible files
  • Shared useMultiSelect hook handles all selection logic with stale-path pruning via useMemo

Test plan

  • E2E: file-tree-multi-select.spec.ts — ctrl-click toggle, escape clear, drag-to-move
  • E2E: changes-panel-multi-select.spec.ts — ctrl-click + bulk bar, bulk stage, escape clear
  • Manual: Shift-click range selection in file tree with expanded folders
  • Manual: Drag multiple selected files onto a directory
  • Manual: Bulk discard with confirmation dialog
  • Manual: Right-click multi-selected files shows "Delete N items"

Screenshots

export-1775918865776 (1)

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds multi-selection and bulk actions to the Files and Changes UIs, multi-node drag-and-drop with optimistic moves/deletes, new useMultiSelect and tree utilities, selection-aware context menus/rows, bulk git handlers wired to session ops, and Playwright E2E tests + locators.

Changes

Cohort / File(s) Summary
Selection Hook & Changes Row
apps/web/hooks/use-multi-select.ts, apps/web/components/task/changes-panel-file-row.tsx
Adds useMultiSelect (Ctrl/Meta/Shift/range/selectAll/clear). changes-panel-file-row gains isSelected/onSelect, data attributes, updated click semantics, and exports DefaultActionButtons + BulkActionBar.
Changes Panel / Timeline
apps/web/components/task/changes-panel-timeline.tsx, apps/web/components/task/changes-panel.tsx
Threaded bulk handlers (onBulkStage/onBulkUnstage/onBulkDiscard) through components; FileListSection uses useMultiSelect, supports Escape to clear, and conditionally shows bulk or default action UI. Bulk handlers call session git ops.
File Browser & Tree UI
apps/web/components/task/file-browser.tsx, apps/web/components/task/file-browser-parts.tsx
File browser wired to useMultiSelect, useAutoExpandAncestors, and useDragAndDrop; adds keyboard shortcuts (Esc, Ctrl/Cmd+A), focusable container, FileTreeView, selection-aware TreeNodeItem/row, and multi-node drag/drop with optimistic updates and rollback.
Tree Utilities
apps/web/components/task/file-tree-utils.ts
New exports: getVisiblePaths, findNodeByPath, and moveNodesInTree for visible-path enumeration, node lookup, and multi-node move/rename operations.
Context Menu & Delete Flow
apps/web/components/task/file-context-menu.tsx
Adds deleteNodeOptimistically, DeleteConfirmDialog, selection-aware props (selectedCount/selectedPaths), bulk-delete flow, and conditional menu UI (hide rename for bulk).
E2E Helpers & Tests
apps/web/e2e/pages/session-page.ts, apps/web/e2e/tests/changes-panel-multi-select.spec.ts, apps/web/e2e/tests/file-tree-multi-select.spec.ts
Adds Playwright locators for tree/changes rows and bulk controls plus E2E specs exercising multi-select, bulk stage/discard, and Escape clearing. Includes GitHelper with retry logic.

Sequence Diagram

sequenceDiagram
    participant User as "User"
    participant UI as "File/Changes UI"
    participant Multi as "useMultiSelect"
    participant Session as "Session Git Ops"

    User->>UI: Click file (plain / Ctrl/Meta / Shift)
    UI->>Multi: handleClick(path, event)
    Multi->>Multi: update selectedPaths (toggle/range/replace)
    Multi-->>UI: selectedPaths
    UI->>UI: render BulkActionBar or DefaultActionButtons

    User->>UI: Click bulk action (Stage/Unstage/Discard)
    UI->>Session: onBulk*(selectedPaths array)
    Session->>Session: git.stageFile/git.unstageFile/git.discard(paths)
    Session-->>UI: success/failure
    UI->>Multi: clearSelection() (on success)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

codex

Poem

🐰 I hopped through nodes and clicked with care,

I toggled friends with Ctrl and Shift to share.
Bulk-stage and discard, optimistic delight,
I moved the trees and rolled back if not right.
A tiny rabbit cheers: code, hops, and light!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive PR description provides summary, test plan, and screenshot, but lacks validation details and diagram sections required by template. Add a Validation section documenting test commands run (e.g., npm test, E2E test execution). Optionally include a Mermaid diagram illustrating the multi-select flow or bulk action state transitions for clarity.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main features added: multi-select and drag-to-move functionality for file tree and changes panel.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/multi-entity-selecti-eyb

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 and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 29, 2026

Greptile Summary

This PR adds Shift/Ctrl multi-select, bulk stage/unstage/discard, and drag-to-move to both the file tree and changes panel. The implementation uses a new shared useMultiSelect hook, a useDragAndDrop hook, and a set of new file-tree utilities (getVisiblePaths, computeMoveTargets, moveNodesInTree). Previously flagged issues (bulk delete missing selected paths, shift-click stale anchor, discard confirmation dialog) are resolved.

  • P1: DiscardDialog renders a blank filename when bulk-discarding exactly 1 file — the isBulk guard uses > 1 instead of >= 1, so the single-file bulk path falls through to the fileToDiscard branch, which is null (set to null by handleBulkDiscardClick), producing "discard all changes to ·." in the confirmation dialog.

Confidence Score: 4/5

Safe to merge after fixing the single-file bulk-discard dialog bug; all other remaining findings are P2 or lower.

One P1 bug remains: DiscardDialog shows a blank filename when bulk-discarding exactly 1 file, because isBulk uses > 1 rather than >= 1. The discard operation itself still executes correctly, but the confirmation text is misleading for a destructive action. Previously flagged issues (bulk-delete coverage, shift-click anchor reset, discard confirmation routing) are all addressed. The core hook, tree utilities, and drag-and-drop logic are solid and well-tested.

apps/web/components/task/changes-panel-dialogs.tsx — fix the isBulk threshold before merging.

Important Files Changed

Filename Overview
apps/web/components/task/changes-panel-dialogs.tsx Adds filesToDiscard to DiscardDialog; P1 bug: isBulk guard uses > 1 instead of >= 1, causing blank filename in the confirmation dialog for single-file bulk discard
apps/web/hooks/use-multi-select.ts New shared hook for range/toggle multi-select; stale-path pruning via useMemo is correct; onSelectionChange called inside state updater (previously flagged, not yet fixed) but has no callers today
apps/web/components/task/file-browser.tsx useDragAndDrop and useSelectionInteractions well-structured; useKeyboardShortcuts re-subscribes on every render because multiSelect object reference changes each time (P2)
apps/web/components/task/file-tree-utils.ts New tree utility functions (getVisiblePaths, computeMoveTargets, moveNodesInTree) are well-tested and handle name-collision deduplication correctly
apps/web/components/task/changes-panel-timeline.tsx Each FileListSection gets its own useMultiSelect instance; selection is automatically pruned when files move; BulkActionBar swaps in for DefaultActionButtons when anything is selected
apps/web/components/task/file-context-menu.tsx Previously-flagged ghost-restore on partial failure still present; new DeleteConfirmDialog correctly handles both directory and bulk-selection cases

Reviews (17): Last reviewed commit: "test: add unit tests for file tree move ..." | Re-trigger Greptile

Comment thread apps/web/components/task/file-context-menu.tsx Outdated
Comment thread apps/web/hooks/use-multi-select.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (4)
apps/web/e2e/tests/file-tree-multi-select.spec.ts (1)

47-50: Minor: Commit message is not escaped in shell command.

The message parameter is interpolated directly into the shell command, which could cause issues if the message contains special characters (quotes, backticks). Since test messages are controlled, this is low risk but could be hardened.

🛡️ Safer alternative using array-based exec
   commit(message: string): string {
-    this.exec(`git commit -m "${message}"`);
+    execSync("git", ["commit", "-m", message], { cwd: this.repoDir, env: this.env });
     return this.exec("git rev-parse HEAD").trim();
   }

Or use execFileSync which handles arguments safely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/e2e/tests/file-tree-multi-select.spec.ts` around lines 47 - 50, The
commit(message: string) method interpolates message into a shell string which
can break for quotes; change it to pass the message as a safe argument instead
of inline interpolation—use the existing exec helper with an array-argument form
or switch to execFileSync/execFile with args (e.g., call git commit -m with
message as a separate arg) and keep returning exec("git rev-parse HEAD").trim();
update the commit function to build the command using arguments rather than
injecting message into the shell string.
apps/web/components/task/file-context-menu.tsx (2)

51-90: fileCount is computed even for bulk deletions where it's unused.

countFilesInTree(node) is called unconditionally via the fileCount prop, but the result is only displayed when !isBulk. Consider computing it conditionally or moving the computation inside the dialog.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/task/file-context-menu.tsx` around lines 51 - 90, The
DeleteConfirmDialog is receiving fileCount (computed by countFilesInTree(node))
even when isBulk is true and the value is unused; fix by computing fileCount
only when needed—either stop passing fileCount for bulk operations or move the
call to countFilesInTree(node) inside DeleteConfirmDialog and wrap it in a lazy
calculation (e.g., useMemo) that runs only when !isBulk and node is present;
update the calling code that currently invokes countFilesInTree and the
DeleteConfirmDialog signature (symbols: DeleteConfirmDialog, countFilesInTree,
fileCount, node, isBulk) so the expensive count is skipped for bulk deletions.

36-49: Consider showing a toast on delete failure for better user feedback.

deleteNodeOptimistically silently reverts on failure. Users won't know why the file reappeared. A brief error toast would improve UX.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/task/file-context-menu.tsx` around lines 36 - 49, The
optimistic delete in deleteNodeOptimistically currently reverts state silently;
update it to notify the user on failure by calling the app's toast/error
notifier (e.g., toast.error or showToast) inside both the .then(!ok) branch and
the .catch branch after you setTree(snapshot). Keep the existing snapshot
rollback via setTree(snapshot) and add a concise error message that includes the
file path (or a brief description) to give context; reference
deleteNodeOptimistically, setTree, onDeleteFile, removeNodeFromTree, and
snapshot when making the change.
apps/web/components/task/changes-panel-timeline.tsx (1)

296-472: Split the new bulk-selection UI out of this file.

This file is now 633 lines, and the added DefaultActionButtons / BulkActionBar logic concentrates too many responsibilities in changes-panel-timeline.tsx. Extracting the bulk-selection controls, or the full multi-select file section, into a dedicated module would make the staged/unstaged flows easier to test and bring the file back in line with the repo limits. As per coding guidelines, "TypeScript code: Files ≤600 lines; functions ≤100 lines; cyclomatic complexity ≤15; cognitive complexity ≤20; nesting depth ≤4; parameters ≤5" and "Frontend: Components must be ≤200 lines; extract to domain components; use composition over props."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/task/changes-panel-timeline.tsx` around lines 296 - 472,
The FileListSection has grown too large—extract the bulk-selection UI into its
own component file: move DefaultActionButtons and BulkActionBar into a new
module (e.g., BulkSelectionControls) and export them (keeping their prop
signatures intact), then import and use them from changes-panel-timeline.tsx;
ensure FileListSection still passes the same props
(actionLabel/isActionLoading/onAction,
onBulkStage/onBulkUnstage/onBulkDiscard/onClearSelection) and preserve
data-testid attributes and behavior (selection clearing after actions) so
staged/unstaged flows and tests continue to work.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/components/task/changes-panel-file-row.tsx`:
- Around line 75-76: Update the E2E test in review-cumulative-diff.spec.ts to
use the new data-testid pattern emitted by the component: replace the old
getByTestId("file-row-review_cumulative_test.txt") (and any similar "file-row-*"
queries) with getByTestId("changes-file-review_cumulative_test.txt") so it
matches the component's data-testid={`changes-file-${file.path}`} and any
waits/asserts target the updated test id.

In `@apps/web/components/task/changes-panel-timeline.tsx`:
- Around line 291-293: The bulk-action handlers (onBulkStage, onBulkUnstage,
onBulkDiscard) currently trigger onClearSelection() immediately after
dispatching, which drops the selection even if the action is cancelled or fails;
change the flow so the component waits for the handler to complete (have the
handlers return a Promise or accept a success callback) and only call
onClearSelection() when the Promise resolves successfully (or the success
callback is invoked), leaving selection intact on rejection/cancel; update
places that call these handlers (the bulk action invoke sites around functions
handling stage/unstage/discard) to await the result and clear selection on
success, and ensure errors or cancellations do not call onClearSelection().
- Around line 456-468: The two bulk-discard buttons are ambiguous because both
render data-testid="bulk-discard"; update the Button in
changes-panel-timeline.tsx (the JSX block that calls
onBulkDiscard/onClearSelection and uses props paths and selectionCount) to
include a variant-scoped test id (e.g. incorporate the section variant prop or a
local identifier such as "staged" vs "unstaged" or a sectionId) so each
FileListSection renders a unique data-testid like "bulk-discard-staged" or
"bulk-discard-unstaged"; ensure the value is derived from an existing prop or
local variable (not hard-coded) so tests can target the correct button.

In `@apps/web/components/task/changes-panel.tsx`:
- Around line 523-525: The bulk handlers
(onBulkStage/onBulkUnstage/onBulkDiscard) currently use "void
git.stageFile(paths)" etc., which drops returned Promises and causes unhandled
rejections; change these to call the same async wrapper used for single-file
actions (or attach .catch(...) handlers) so errors are caught and surfaced
(e.g., show toast/log) — update the handlers referencing git.stageFile,
git.unstageFile, and git.discard to await or append .catch and reuse the
single-file error handling routine to ensure consistent behavior.

In `@apps/web/components/task/file-browser.tsx`:
- Around line 221-225: The drag state can remain stuck because handleDragEnd
only runs on the element's dragend; update the logic so the same cleanup
(setIsDragging(false), setDragOverPath(null), dragPathsRef.current = []) is also
performed when the pointer leaves the entire tree container and when global
drag/keyboard cancellation occurs: add/extend handleDragLeave (or a new
onContainerDragLeave) to detect leaving the full container and call the same
cleanup, and register document-level listeners for 'dragend' and 'keyup'
(checking for Escape) during drag start to call handleDragEnd cleanup and remove
those listeners on cleanup; reference handleDragEnd, handleDragLeave,
dragPathsRef, setIsDragging and setDragOverPath to locate where to add the
duplicated cleanup and listener registration/removal.

In `@apps/web/components/task/file-context-menu.tsx`:
- Around line 116-119: handleConfirmDelete currently always calls
deleteNodeOptimistically with node.path, so when isBulk is true the dialog
"Delete N items?" only deletes the single context-menu target; update the
component API and handler so bulk deletes remove all selected paths—either add a
new prop selectedPaths: string[] (or similar) and pass that array into a bulk
delete helper, or add a dedicated onDeleteFiles(selectedPaths) callback and call
it from handleConfirmDelete when isBulk true; modify handleConfirmDelete to
check isBulk and call the bulk deletion path (using selectedPaths or
onDeleteFiles) otherwise fall back to deleteNodeOptimistically(node.path), and
ensure the prop names (selectedCount, selectedPaths, onDeleteFiles) are wired
where the component is used.

In `@apps/web/components/task/file-tree-utils.ts`:
- Around line 117-146: The moveNodesInTree implementation can produce identical
target paths when sourcePaths contain files with the same basename, causing the
second insertNodeInTree call to overwrite the first; update moveNodesInTree to
detect duplicate computed newPath values before calling
renameSubtree/insertNodeInTree and either (a) abort with a clear error listing
colliding basenames or (b) disambiguate by generating unique target names (e.g.,
append numeric suffixes like "name (1).ext") and use those unique newPath values
when calling renameSubtree so insertNodeInTree receives non-colliding nodes;
reference moveNodesInTree, sourcePaths, renameSubtree, insertNodeInTree and
ensure the chosen strategy updates nodesToMove accordingly so
removeNodeFromTree/insert operations remain consistent.

In `@apps/web/e2e/tests/changes-panel-multi-select.spec.ts`:
- Around line 18-33: The exec method currently force-deletes Git's index.lock
(lockPath) which can corrupt a concurrently running git process; remove the
fs.existsSync(lockPath) and fs.unlinkSync(lockPath) calls and instead detect the
presence of lockPath, and when present treat it as lock contention: wait/retry
(use the existing execSync("sleep 0.2") delay) up to three attempts before
failing; keep the try/catch and the existing message check for "index.lock" and
only retry/continue when lock contention is detected (and attempt < 2),
otherwise rethrow the error — reference function exec, variable lockPath, and
the execSync("sleep 0.2") retry logic to locate changes.

---

Nitpick comments:
In `@apps/web/components/task/changes-panel-timeline.tsx`:
- Around line 296-472: The FileListSection has grown too large—extract the
bulk-selection UI into its own component file: move DefaultActionButtons and
BulkActionBar into a new module (e.g., BulkSelectionControls) and export them
(keeping their prop signatures intact), then import and use them from
changes-panel-timeline.tsx; ensure FileListSection still passes the same props
(actionLabel/isActionLoading/onAction,
onBulkStage/onBulkUnstage/onBulkDiscard/onClearSelection) and preserve
data-testid attributes and behavior (selection clearing after actions) so
staged/unstaged flows and tests continue to work.

In `@apps/web/components/task/file-context-menu.tsx`:
- Around line 51-90: The DeleteConfirmDialog is receiving fileCount (computed by
countFilesInTree(node)) even when isBulk is true and the value is unused; fix by
computing fileCount only when needed—either stop passing fileCount for bulk
operations or move the call to countFilesInTree(node) inside DeleteConfirmDialog
and wrap it in a lazy calculation (e.g., useMemo) that runs only when !isBulk
and node is present; update the calling code that currently invokes
countFilesInTree and the DeleteConfirmDialog signature (symbols:
DeleteConfirmDialog, countFilesInTree, fileCount, node, isBulk) so the expensive
count is skipped for bulk deletions.
- Around line 36-49: The optimistic delete in deleteNodeOptimistically currently
reverts state silently; update it to notify the user on failure by calling the
app's toast/error notifier (e.g., toast.error or showToast) inside both the
.then(!ok) branch and the .catch branch after you setTree(snapshot). Keep the
existing snapshot rollback via setTree(snapshot) and add a concise error message
that includes the file path (or a brief description) to give context; reference
deleteNodeOptimistically, setTree, onDeleteFile, removeNodeFromTree, and
snapshot when making the change.

In `@apps/web/e2e/tests/file-tree-multi-select.spec.ts`:
- Around line 47-50: The commit(message: string) method interpolates message
into a shell string which can break for quotes; change it to pass the message as
a safe argument instead of inline interpolation—use the existing exec helper
with an array-argument form or switch to execFileSync/execFile with args (e.g.,
call git commit -m with message as a separate arg) and keep returning exec("git
rev-parse HEAD").trim(); update the commit function to build the command using
arguments rather than injecting message into the shell string.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 996a5f58-5125-4575-a104-bbfb68b529dd

📥 Commits

Reviewing files that changed from the base of the PR and between 3a71ae2 and d7bdec1.

📒 Files selected for processing (11)
  • apps/web/components/task/changes-panel-file-row.tsx
  • apps/web/components/task/changes-panel-timeline.tsx
  • apps/web/components/task/changes-panel.tsx
  • apps/web/components/task/file-browser-parts.tsx
  • apps/web/components/task/file-browser.tsx
  • apps/web/components/task/file-context-menu.tsx
  • apps/web/components/task/file-tree-utils.ts
  • apps/web/e2e/pages/session-page.ts
  • apps/web/e2e/tests/changes-panel-multi-select.spec.ts
  • apps/web/e2e/tests/file-tree-multi-select.spec.ts
  • apps/web/hooks/use-multi-select.ts

Comment thread apps/web/components/task/changes-panel-file-row.tsx Outdated
Comment thread apps/web/components/task/changes-panel-timeline.tsx
Comment thread apps/web/components/task/changes-panel-timeline.tsx Outdated
Comment thread apps/web/components/task/changes-panel.tsx Outdated
Comment thread apps/web/components/task/file-browser.tsx
Comment thread apps/web/components/task/file-context-menu.tsx Outdated
Comment thread apps/web/components/task/file-tree-utils.ts
Comment thread apps/web/e2e/tests/changes-panel-multi-select.spec.ts Outdated
Comment thread apps/web/components/task/changes-panel-file-row.tsx
Comment thread apps/web/components/task/file-context-menu.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
apps/web/components/task/file-browser-parts.tsx (1)

134-136: ⚠️ Potential issue | 🟠 Major

Nested tree nodes inherit parent's selection state instead of being independently evaluated.

At line 417, FileTreeView correctly evaluates isSelected?.(child.path) to a boolean for each direct child. However, at line 135, TreeNodeChildren spreads {...props} (containing the parent's evaluated boolean) to nested TreeNodeItem elements. This causes nested children to receive the parent's selection state rather than their own path being evaluated.

Since isSelected is used in TreeNodeRow line 153 for styling (isSelected && "bg-accent"), descendants of a selected node incorrectly receive the same blue background color.

The root cause is a type mismatch: FileBrowserContentAreaProps.isSelected is a function, but TreeNodeItemProps.isSelected is a boolean. The evaluation happens at the top level, then the boolean spreads to all descendants.

Fix: Either pass the original function through the tree (requiring type changes), or evaluate at render time in each TreeNodeItem rather than spreading the computed result.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/task/file-browser-parts.tsx` around lines 134 - 136, The
bug is that TreeNodeChildren spreads {...props} which contains a boolean
isSelected evaluated at the parent, so nested TreeNodeItem instances inherit the
parent's selection state; fix by ensuring each TreeNodeItem evaluates selection
by path instead of receiving the parent's boolean: remove isSelected from the
spread and pass a per-child computed boolean (e.g. isSelected?.(child.path)) or
better, change the prop contract to forward the original
FileBrowserContentAreaProps.isSelected function down and have
TreeNodeItem/TreeNodeRow call isSelected(path) themselves (update types for
TreeNodeItemProps to accept a function), and adjust TreeNodeRow usage of
isSelected accordingly so descendants compute their own selection rather than
inheriting the parent value.
🧹 Nitpick comments (2)
apps/web/hooks/use-multi-select.ts (1)

46-52: setSelectedPaths bypasses pruning — consider documenting this behavior.

When setSelectedPaths is called externally, it sets rawSelection directly without pruning. The pruned selectedPaths will be derived on the next render, but onSelectionChange is immediately called with the unpruned value.

This is likely intentional (callers like drag-and-drop clear selection with new Set()), but worth documenting for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/hooks/use-multi-select.ts` around lines 46 - 52, The
setSelectedPaths callback directly assigns to rawSelection and invokes
onSelectionChange with the unpruned Set, which bypasses the pruning logic that
produces selectedPaths; add a clear comment or JSDoc above the setSelectedPaths
definition (referencing setSelectedPaths, setRawSelection, rawSelection,
selectedPaths, and onSelectionChange) stating that this intentionally skips
pruning so callers (e.g., drag-and-drop) can pass raw selections like new Set(),
and note that pruned selectedPaths will be derived on the next render and that
onSelectionChange receives the unpruned value immediately.
apps/web/components/task/file-browser.tsx (1)

299-319: Consider stabilizing multiSelect dependency in useKeyboardShortcuts.

The multiSelect object is recreated on each render, causing the effect to re-run and re-attach the listener. Since only clearSelection and selectAll are used, consider passing those callbacks directly or memoizing the hook's return value in useMultiSelect.

This is a minor performance consideration — the current implementation is functionally correct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/task/file-browser.tsx` around lines 299 - 319, The effect
in useKeyboardShortcuts is re-running because the multiSelect object identity
changes each render; change the hook to accept stable callbacks or memoize
multiSelect: either (A) change the signature of
useKeyboardShortcuts(containerRef, { clearSelection, selectAll }: {
clearSelection: () => void; selectAll: () => void }) and pass
multiSelect.clearSelection and multiSelect.selectAll from the caller, or (B)
ensure useMultiSelect returns a stable object by wrapping its handlers in
useCallback/useMemo so multiSelect (the value returned by useMultiSelect) has
stable identity; update references to multiSelect.clearSelection and
multiSelect.selectAll accordingly to use the new stable callbacks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/components/task/file-context-menu.tsx`:
- Around line 120-126: When handling bulk deletes in file-context-menu, avoid
deleting parents before their nested children: before looping over selectedPaths
in the isBulk branch, sort the paths by depth descending and remove any paths
that are descendants of another selected path (deduplicate by ancestor
containment) so deleteNodeOptimistically(tree, setTree, p, onDeleteFile) is
invoked from deepest to shallowest on unique targets; update the selectedPaths
iteration to use this processed list to prevent removeNodeFromTree/restore
snapshot failures.

---

Outside diff comments:
In `@apps/web/components/task/file-browser-parts.tsx`:
- Around line 134-136: The bug is that TreeNodeChildren spreads {...props} which
contains a boolean isSelected evaluated at the parent, so nested TreeNodeItem
instances inherit the parent's selection state; fix by ensuring each
TreeNodeItem evaluates selection by path instead of receiving the parent's
boolean: remove isSelected from the spread and pass a per-child computed boolean
(e.g. isSelected?.(child.path)) or better, change the prop contract to forward
the original FileBrowserContentAreaProps.isSelected function down and have
TreeNodeItem/TreeNodeRow call isSelected(path) themselves (update types for
TreeNodeItemProps to accept a function), and adjust TreeNodeRow usage of
isSelected accordingly so descendants compute their own selection rather than
inheriting the parent value.

---

Nitpick comments:
In `@apps/web/components/task/file-browser.tsx`:
- Around line 299-319: The effect in useKeyboardShortcuts is re-running because
the multiSelect object identity changes each render; change the hook to accept
stable callbacks or memoize multiSelect: either (A) change the signature of
useKeyboardShortcuts(containerRef, { clearSelection, selectAll }: {
clearSelection: () => void; selectAll: () => void }) and pass
multiSelect.clearSelection and multiSelect.selectAll from the caller, or (B)
ensure useMultiSelect returns a stable object by wrapping its handlers in
useCallback/useMemo so multiSelect (the value returned by useMultiSelect) has
stable identity; update references to multiSelect.clearSelection and
multiSelect.selectAll accordingly to use the new stable callbacks.

In `@apps/web/hooks/use-multi-select.ts`:
- Around line 46-52: The setSelectedPaths callback directly assigns to
rawSelection and invokes onSelectionChange with the unpruned Set, which bypasses
the pruning logic that produces selectedPaths; add a clear comment or JSDoc
above the setSelectedPaths definition (referencing setSelectedPaths,
setRawSelection, rawSelection, selectedPaths, and onSelectionChange) stating
that this intentionally skips pruning so callers (e.g., drag-and-drop) can pass
raw selections like new Set(), and note that pruned selectedPaths will be
derived on the next render and that onSelectionChange receives the unpruned
value immediately.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 38a4f12b-3bce-40c9-8e97-d6625ff0bc6e

📥 Commits

Reviewing files that changed from the base of the PR and between d7bdec1 and f672110.

📒 Files selected for processing (9)
  • apps/web/components/task/changes-panel-file-row.tsx
  • apps/web/components/task/changes-panel-timeline.tsx
  • apps/web/components/task/file-browser-parts.tsx
  • apps/web/components/task/file-browser.tsx
  • apps/web/components/task/file-context-menu.tsx
  • apps/web/e2e/pages/session-page.ts
  • apps/web/e2e/tests/changes-panel-multi-select.spec.ts
  • apps/web/e2e/tests/file-tree-multi-select.spec.ts
  • apps/web/hooks/use-multi-select.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/e2e/tests/changes-panel-multi-select.spec.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/e2e/pages/session-page.ts
  • apps/web/e2e/tests/file-tree-multi-select.spec.ts

Comment thread apps/web/components/task/file-context-menu.tsx
Comment thread apps/web/hooks/use-multi-select.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/web/components/task/file-browser-parts.tsx (1)

386-435: Good extraction of FileTreeView component.

The extraction improves readability and keeps FileBrowserContentArea focused on routing between search results, load states, and tree view. One minor observation: line 420 evaluates props.isSelected?.(child.path) on every render for each child. This is fine for typical tree sizes, but if performance becomes a concern with very large trees, consider passing the function reference and evaluating inside TreeNodeItem instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/task/file-browser-parts.tsx` around lines 386 - 435,
FileTreeView currently calls props.isSelected for every child
(isSelected={props.isSelected?.(child.path)}), causing per-node evaluation on
each render; instead pass the function reference down and let TreeNodeItem
evaluate it. Change the prop passed to TreeNodeItem to
isSelected={props.isSelected} (or isSelectedFn if you prefer) and update
TreeNodeItem to call that function with the node path (e.g., const selected =
props.isSelected?.(node.path)) so evaluation happens inside TreeNodeItem and can
be optimized/memoized there.
apps/web/components/task/changes-panel-file-row.tsx (1)

266-266: Unnecessary useMemo for array conversion.

The useMemo with [selectedPaths] dependency recreates the array whenever the Set reference changes, which happens on every selection change. Since [...selectedPaths] is a trivial O(n) operation that's only used when a button is clicked, the memoization adds overhead without benefit.

Consider inlining the spread in the click handlers instead:

♻️ Proposed simplification
 export function BulkActionBar({
   variant,
   selectionCount,
   selectedPaths,
   onBulkStage,
   onBulkUnstage,
   onBulkDiscard,
 }: {
   variant: "unstaged" | "staged";
   selectionCount: number;
   selectedPaths: Set<string>;
   onBulkStage?: (paths: string[]) => void;
   onBulkUnstage?: (paths: string[]) => void;
   onBulkDiscard?: (paths: string[]) => void;
 }) {
-  const paths = useMemo(() => [...selectedPaths], [selectedPaths]);
 
   return (
     <div data-testid={`bulk-actions-${variant}`} className="flex items-center gap-1.5">
       <span className="text-[11px] text-muted-foreground">{selectionCount} selected</span>
       {variant === "unstaged" && onBulkStage && (
         <Button
           data-testid="bulk-stage"
           size="sm"
           variant="outline"
           className="h-6 text-[11px] px-2.5 gap-1 cursor-pointer"
-          onClick={() => onBulkStage(paths)}
+          onClick={() => onBulkStage([...selectedPaths])}
         >
           Stage {selectionCount}
         </Button>
       )}
       {variant === "staged" && onBulkUnstage && (
         <Button
           data-testid={`bulk-unstage-${variant}`}
           size="sm"
           variant="outline"
           className="h-6 text-[11px] px-2.5 gap-1 cursor-pointer"
-          onClick={() => onBulkUnstage(paths)}
+          onClick={() => onBulkUnstage([...selectedPaths])}
         >
           Unstage {selectionCount}
         </Button>
       )}
       {onBulkDiscard && (
         <Button
           data-testid={`bulk-discard-${variant}`}
           size="sm"
           variant="outline"
           className="h-6 text-[11px] px-2.5 gap-1 cursor-pointer text-destructive hover:text-destructive"
-          onClick={() => onBulkDiscard(paths)}
+          onClick={() => onBulkDiscard([...selectedPaths])}
         >
           Discard {selectionCount}
         </Button>
       )}
     </div>
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/task/changes-panel-file-row.tsx` at line 266, The useMemo
call creating const paths = useMemo(() => [...selectedPaths], [selectedPaths])
is unnecessary and should be removed; instead, directly spread selectedPaths
where needed (e.g., in the click handlers that currently reference paths) to
build a fresh array at the moment of the button click — replace uses of paths
with [...selectedPaths] (or a locally named array inside the handler) and delete
the useMemo import/definition to avoid extra overhead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/components/task/changes-panel-file-row.tsx`:
- Line 266: The useMemo call creating const paths = useMemo(() =>
[...selectedPaths], [selectedPaths]) is unnecessary and should be removed;
instead, directly spread selectedPaths where needed (e.g., in the click handlers
that currently reference paths) to build a fresh array at the moment of the
button click — replace uses of paths with [...selectedPaths] (or a locally named
array inside the handler) and delete the useMemo import/definition to avoid
extra overhead.

In `@apps/web/components/task/file-browser-parts.tsx`:
- Around line 386-435: FileTreeView currently calls props.isSelected for every
child (isSelected={props.isSelected?.(child.path)}), causing per-node evaluation
on each render; instead pass the function reference down and let TreeNodeItem
evaluate it. Change the prop passed to TreeNodeItem to
isSelected={props.isSelected} (or isSelectedFn if you prefer) and update
TreeNodeItem to call that function with the node path (e.g., const selected =
props.isSelected?.(node.path)) so evaluation happens inside TreeNodeItem and can
be optimized/memoized there.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b6b01e86-b932-4bfe-94e3-96fd0138b729

📥 Commits

Reviewing files that changed from the base of the PR and between f672110 and f82751e.

📒 Files selected for processing (4)
  • apps/web/components/task/changes-panel-file-row.tsx
  • apps/web/components/task/changes-panel-timeline.tsx
  • apps/web/components/task/file-browser-parts.tsx
  • apps/web/e2e/tests/changes-panel-multi-select.spec.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/e2e/tests/changes-panel-multi-select.spec.ts

@zeval zeval force-pushed the feature/multi-entity-selecti-eyb branch from f82751e to 7dd75d7 Compare April 5, 2026 21:03
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
apps/web/components/task/file-context-menu.tsx (1)

120-126: ⚠️ Potential issue | 🟡 Minor

Bulk delete may fail silently for nested paths.

When deleting multiple paths where one is a parent of another (e.g., dir and dir/file.txt), the iteration order is non-deterministic. If the parent directory is deleted first, the nested file's removeNodeFromTree will silently fail to find the node, and snapshot restoration would be incorrect.

Consider sorting paths by depth (deepest first) or deduplicating paths that are descendants of other selected paths.

🛡️ Proposed fix to handle nested paths
   if (isBulk && selectedPaths) {
+    // Sort deepest paths first to avoid parent deletion invalidating child paths
+    const sortedPaths = [...selectedPaths].sort(
+      (a, b) => b.split("/").length - a.split("/").length
+    );
+    for (const p of sortedPaths) {
-    for (const p of selectedPaths) {
       deleteNodeOptimistically(tree, setTree, p, onDeleteFile);
     }
   } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/task/file-context-menu.tsx` around lines 120 - 126, The
bulk-delete loop can fail when selectedPaths contains parent and descendant
paths; before calling deleteNodeOptimistically (used with tree, setTree,
onDeleteFile) sort and deduplicate selectedPaths so deeper paths are deleted
before their parents: compute path depth (e.g., count separators), sort
descending, and also remove any path that is a descendant of another selected
path (keep only the ancestor or only the deepest entry as desired) so
removeNodeFromTree inside deleteNodeOptimistically will always find the node and
snapshots restore correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/web/components/task/file-context-menu.tsx`:
- Around line 120-126: The bulk-delete loop can fail when selectedPaths contains
parent and descendant paths; before calling deleteNodeOptimistically (used with
tree, setTree, onDeleteFile) sort and deduplicate selectedPaths so deeper paths
are deleted before their parents: compute path depth (e.g., count separators),
sort descending, and also remove any path that is a descendant of another
selected path (keep only the ancestor or only the deepest entry as desired) so
removeNodeFromTree inside deleteNodeOptimistically will always find the node and
snapshots restore correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5b154159-3d0d-4526-a0cb-3f343180f571

📥 Commits

Reviewing files that changed from the base of the PR and between f82751e and 7dd75d7.

📒 Files selected for processing (11)
  • apps/web/components/task/changes-panel-file-row.tsx
  • apps/web/components/task/changes-panel-timeline.tsx
  • apps/web/components/task/changes-panel.tsx
  • apps/web/components/task/file-browser-parts.tsx
  • apps/web/components/task/file-browser.tsx
  • apps/web/components/task/file-context-menu.tsx
  • apps/web/components/task/file-tree-utils.ts
  • apps/web/e2e/pages/session-page.ts
  • apps/web/e2e/tests/changes-panel-multi-select.spec.ts
  • apps/web/e2e/tests/file-tree-multi-select.spec.ts
  • apps/web/hooks/use-multi-select.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/e2e/tests/file-tree-multi-select.spec.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/web/components/task/changes-panel.tsx
  • apps/web/components/task/file-tree-utils.ts
  • apps/web/hooks/use-multi-select.ts
  • apps/web/e2e/pages/session-page.ts

@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

Claude finished @zeval's task in 4m 33s —— View job


Findings

Blocker (must fix before merge)

  1. Nested file selection state brokenapps/web/components/task/file-browser-parts.tsx:415 → comment

    • Issue: FileTreeView evaluates isSelected to a boolean per top-level child, then TreeNodeChildren spreads {...props} including that boolean to all recursive children. Files inside expanded directories inherit the parent directory's selection state instead of their own.
    • Why: Multi-select is visually broken for any project with subdirectories — the core feature doesn't work in the most common case.
    • Fix: Change TreeNodeItemProps.isSelected from boolean to (path: string) => boolean, pass the function reference through the tree unchanged, and evaluate it at TreeNodeItem render time.
  2. Bulk discard has no confirmation dialogapps/web/components/task/changes-panel-file-row.tsx:295 → comment

    • Issue: BulkActionBar's "Discard N" button calls onBulkDiscard(paths) immediately with no confirmation. Single-file discard shows a dialog via dialogs.handleDiscardClick. The PR test plan lists "Bulk discard with confirmation dialog" as a manual test requirement.
    • Why: Discarding unstaged changes is irreversible — one accidental click permanently loses all selected work.
    • Fix: Add a confirmation step before calling onBulkDiscard, either via a local dialog in BulkActionBar or by threading a confirmation callback from changes-panel.tsx.

Suggestion (recommended, doesn't block)

  1. Shared snapshot causes ghost-restore on partial bulk delete failureapps/web/components/task/file-context-menu.tsx:122 → comment

    • All deleteNodeOptimistically calls in the bulk loop share the same tree snapshot. If any single delete fails, the rollback restores the full original tree including already-successfully-deleted files.
  2. onSelectionChange called inside React state updaterapps/web/hooks/use-multi-select.ts:95 → comment

    • React Strict Mode double-invokes updaters; onSelectionChange would fire twice per click in development but once in production. Not breaking today (no callers), but will behave incorrectly when wired up.
  3. Keyboard listener re-registers on every renderapps/web/components/task/file-browser.tsx:318 → comment

    • multiSelect object reference is new every render; passing stable clearSelection/selectAll callbacks would fix the listener churn.
  4. Unnecessary useMemo in BulkActionBarapps/web/components/task/changes-panel-file-row.tsx:262 → comment

    • selectedPaths changes reference on every selection change, so useMemo provides no benefit here.

Summary

Severity Count
Blocker 2
Suggestion 4

Verdict: Blocked — fix blockers first

The nested selection highlight bug (#1) means multi-select is visually non-functional for any file inside an expanded directory — the central feature of this PR. Bulk discard without confirmation (#2) is an irreversible data-loss risk that the PR's own test plan calls out as a requirement.

Comment thread apps/web/components/task/file-browser-parts.tsx Outdated
Comment thread apps/web/components/task/changes-panel-file-row.tsx
Comment thread apps/web/hooks/use-multi-select.ts Outdated
Comment thread apps/web/components/task/file-browser.tsx Outdated
Comment thread apps/web/components/task/file-context-menu.tsx Outdated
Comment thread apps/web/components/task/changes-panel-file-row.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/web/e2e/tests/changes-panel-multi-select.spec.ts (1)

84-223: Please cover the other branches of the new click contract.

This suite only proves Ctrl/Cmd toggling. The PR also changes Shift-click range selection and the plain-click fallback that should clear selection and still open the diff. If handleClick ever starts always reporting the click as “consumed”, these tests keep passing while row navigation regresses.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/e2e/tests/changes-panel-multi-select.spec.ts` around lines 84 - 223,
Add tests that exercise the other click-contract branches: add one test using
Shift-click on session.changesFileRow(...) to select a contiguous range (click
first, Shift-click last) and assert intermediate rows are selected and bulk
actions appear; add another test that verifies a plain click on a non-selected
row clears any existing selection (use session.changesSelectedRows()) but still
opens the diff panel (assert diff opens) to guard the handleClick contract; use
the same helpers used in this file (session.changesFileRow,
session.changesBulkActionBar, session.changesSelectedRows,
session.clickTab("Changes")) so the tests fail if handleClick incorrectly always
reports clicks as consumed.
apps/web/components/task/changes-panel-file-row.tsx (1)

221-295: Extract the shared action-button classes.

"h-6 text-[11px] px-2.5 gap-1 cursor-pointer" is now repeated across DefaultActionButtons and BulkActionBar, so the next styling change has four places to drift.

♻️ Suggested extraction
+const ACTION_BUTTON_CLASS = "h-6 text-[11px] px-2.5 gap-1 cursor-pointer";
+
 export function DefaultActionButtons({
   actionLabel,
   isActionLoading,
@@
       <Button
         size="sm"
         variant="outline"
-        className="h-6 text-[11px] px-2.5 gap-1 cursor-pointer"
+        className={ACTION_BUTTON_CLASS}
         onClick={onAction}
         disabled={isActionLoading}
       >
@@
         <Button
           size="sm"
           variant="outline"
-          className="h-6 text-[11px] px-2.5 gap-1 cursor-pointer"
+          className={ACTION_BUTTON_CLASS}
           onClick={onSecondaryAction}
           disabled={isSecondaryActionLoading}
         >
@@
         <Button
           data-testid="bulk-stage"
           size="sm"
           variant="outline"
-          className="h-6 text-[11px] px-2.5 gap-1 cursor-pointer"
+          className={ACTION_BUTTON_CLASS}
           onClick={() => onBulkStage(paths)}
         >
@@
         <Button
           data-testid={`bulk-unstage-${variant}`}
           size="sm"
           variant="outline"
-          className="h-6 text-[11px] px-2.5 gap-1 cursor-pointer"
+          className={ACTION_BUTTON_CLASS}
           onClick={() => onBulkUnstage(paths)}
         >

As per coding guidelines, "No duplicated strings (≥4 occurrences)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/task/changes-panel-file-row.tsx` around lines 221 - 295,
The repeated class string "h-6 text-[11px] px-2.5 gap-1 cursor-pointer" should
be extracted to a single constant and reused in both DefaultActionButtons and
BulkActionBar; add e.g. const ACTION_BUTTON_CLASSES = "h-6 text-[11px] px-2.5
gap-1 cursor-pointer" near the top of the module and replace the literal
className values in DefaultActionButtons (the Button in that component) and all
Buttons inside BulkActionBar with className={ACTION_BUTTON_CLASSES} (or
concatenated when additional classes like text-destructive are required) so the
style is centralized and not duplicated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/components/task/changes-panel-file-row.tsx`:
- Around line 221-295: The repeated class string "h-6 text-[11px] px-2.5 gap-1
cursor-pointer" should be extracted to a single constant and reused in both
DefaultActionButtons and BulkActionBar; add e.g. const ACTION_BUTTON_CLASSES =
"h-6 text-[11px] px-2.5 gap-1 cursor-pointer" near the top of the module and
replace the literal className values in DefaultActionButtons (the Button in that
component) and all Buttons inside BulkActionBar with
className={ACTION_BUTTON_CLASSES} (or concatenated when additional classes like
text-destructive are required) so the style is centralized and not duplicated.

In `@apps/web/e2e/tests/changes-panel-multi-select.spec.ts`:
- Around line 84-223: Add tests that exercise the other click-contract branches:
add one test using Shift-click on session.changesFileRow(...) to select a
contiguous range (click first, Shift-click last) and assert intermediate rows
are selected and bulk actions appear; add another test that verifies a plain
click on a non-selected row clears any existing selection (use
session.changesSelectedRows()) but still opens the diff panel (assert diff
opens) to guard the handleClick contract; use the same helpers used in this file
(session.changesFileRow, session.changesBulkActionBar,
session.changesSelectedRows, session.clickTab("Changes")) so the tests fail if
handleClick incorrectly always reports clicks as consumed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 418a13a5-6940-4240-a932-4a404247dab5

📥 Commits

Reviewing files that changed from the base of the PR and between 7dd75d7 and 7e55b54.

📒 Files selected for processing (5)
  • apps/web/components/task/changes-panel-file-row.tsx
  • apps/web/components/task/file-browser-parts.tsx
  • apps/web/e2e/tests/changes-panel-multi-select.spec.ts
  • apps/web/e2e/tests/file-tree-multi-select.spec.ts
  • apps/web/hooks/use-multi-select.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/e2e/tests/file-tree-multi-select.spec.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/hooks/use-multi-select.ts
  • apps/web/components/task/file-browser-parts.tsx

Comment thread apps/web/components/task/changes-panel-file-row.tsx
zeval added 10 commits April 9, 2026 22:01
Add Shift-click range and Ctrl/Cmd-click toggle selection to the file
tree and staged/unstaged changes sections. Includes bulk actions
(delete, stage, unstage, discard), drag-and-drop file moving into
directories, keyboard shortcuts (Escape, Ctrl+A), and E2E tests.
- Keep original file-row-* data-testid for backwards compat
- Remove eager onClearSelection from bulk actions (auto-pruned by hook)
- Variant-scope bulk-discard/bulk-unstage test ids
- Remove index.lock deletion from E2E GitHelper retry loop
- Create files AFTER opening session so WS detects git status changes
- Add proper git env vars (GIT_AUTHOR_NAME, etc.) matching existing tests
- Use element.press("Escape") instead of page keyboard for proper focus
- Remove drag-to-directory test (Playwright dragTo doesn't support HTML5
  dataTransfer events needed by our DnD implementation)
…andler

- Use text-accent-foreground on selected rows for dark mode readability
- Override child span colors with [&_span]:text-accent-foreground
- Use hover:bg-accent/80 instead of hover:bg-muted on selected items
- Skip selection handling on right-click (button === 2) so context menu
  works without clearing multi-selection
Plain click now clears any selection and performs the original action
(open file / toggle folder / open diff). Selection only happens with
modifier keys. handleClick returns boolean to indicate whether the
click was consumed by selection logic.
@zeval zeval force-pushed the feature/multi-entity-selecti-eyb branch from 7e55b54 to 33aaf14 Compare April 9, 2026 21:02
@claude
Copy link
Copy Markdown

claude bot commented Apr 9, 2026

Claude finished @zeval's task in 4m 10s —— View job


Findings

Blocker (must fix before merge)

  1. Nested file selection is visually brokenapps/web/components/task/file-browser-parts.tsx:420 → comment

    • Issue: FileTreeView evaluates isSelected?.(child.path) to a boolean for top-level nodes. TreeNodeChildren then spreads {...props} to all recursive children — so nested files always inherit the parent directory's evaluated boolean, never their own path evaluated.
    • Why: Multi-select visual feedback is broken for any file inside an expanded directory — the central feature of the PR fails in the most common case. Ctrl+click src/index.ts adds it to selectedPaths but the row renders unhighlighted.
    • Fix: Change TreeNodeItemProps.isSelected from boolean to (path: string) => boolean. Pass the function reference from FileTreeView (remove the ?.(child.path) call), and evaluate it in TreeNodeItem before passing to TreeNodeRow: isSelected={props.isSelected?.(node.path)}.
  2. Bulk discard fires without confirmationapps/web/components/task/changes-panel-file-row.tsx:295 → comment

    • Issue: BulkActionBar's "Discard N" button calls onBulkDiscard(paths) immediately. Single-file discard goes through dialogs.handleDiscardClick(path) which shows a confirmation dialog; bulk skips it entirely.
    • Why: Discarding unstaged changes is irreversible — one accidental click permanently loses all selected work. The PR test plan explicitly lists "Manual: Bulk discard with confirmation dialog" as a requirement.
    • Fix: Route onBulkDiscard through the existing discard dialog (add a handleBulkDiscardClick(paths) to dialogs) or add local AlertDialog state in BulkActionBar following the same pattern used in file-context-menu.tsx.
  3. Missing unit tests for new hook and utility functionsapps/web/hooks/use-multi-select.ts, apps/web/components/task/file-tree-utils.ts

    • Issue: useMultiSelect has non-trivial selection logic (Ctrl+click toggle, Shift+click range, stale-path pruning, anchor tracking). New utility functions getVisiblePaths, findNodeByPath, moveNodesInTree have tree-traversal logic with edge cases. No *.test.ts files exist for any of these.
    • Why: Per CLAUDE.md, hooks and utility functions require unit tests. E2E tests cover integration but not the selection logic edge cases (range with out-of-bounds anchor, pruning when items change, move collision detection).
    • Fix: Add use-multi-select.test.ts covering toggle, range, selectAll, clear, and pruning. Add tests for the new file-tree-utils.ts exports. Use /tdd to drive this.

Suggestion (recommended, doesn't block)

  1. onSelectionChange called inside React state updaterapps/web/hooks/use-multi-select.ts:95 → comment

    • React Strict Mode double-invokes updaters; onSelectionChange would fire twice per click in dev but once in prod. No callers today, but this will bite when wired up.
  2. Keyboard listener re-registers on every renderapps/web/components/task/file-browser.tsx:318 → comment

    • multiSelect is a new object reference every render; passing clearSelection/selectAll directly as deps would stop the churn.
  3. Shared snapshot causes ghost-restore on partial bulk deleteapps/web/components/task/file-context-menu.tsx:122 → comment

    • All iterations in the bulk delete loop share the same tree snapshot. A partial failure rolls back the entire tree including already-successful deletes. Use a single Promise.all like executeMoveFiles does.
  4. Unnecessary useMemo in BulkActionBarapps/web/components/task/changes-panel-file-row.tsx:262 → comment

    • selectedPaths changes reference on every toggle, so the memo provides no benefit. Inline [...selectedPaths] in the click handlers.

Summary

Severity Count
Blocker 3
Suggestion 4

Verdict: Blocked — fix blockers first

The nested selection highlight bug (#1) means multi-select is visually non-functional for any file inside an expanded directory — the core feature doesn't work in the most common case. Bulk discard without confirmation (#2) is an irreversible data-loss risk the PR's own test plan flags as a requirement. Missing unit tests (#3) are required per CLAUDE.md for hooks and utility functions.

@claude
Copy link
Copy Markdown

claude bot commented Apr 11, 2026

Claude finished @zeval's task in 3m 53s —— View job


PR Review

  • Read skill instructions
  • Gather context & read changed files
  • Analyze code
  • Post inline comments
  • Post findings report

Findings

Blocker (must fix before merge)

  1. Missing unit tests for useMultiSelect hookapps/web/hooks/use-multi-select.ts:147 → comment
    • Issue: No use-multi-select.test.ts exists. The hook has non-trivial, stateful selection logic (Ctrl toggle, Shift range, stale-path pruning, anchor tracking) that has driven multiple bug-fix cycles across this PR's history.
    • Why: Per CLAUDE.md, hooks require *.test.ts coverage. E2E tests confirm integration paths but can't catch hook-internal edge cases — several regressions found in earlier reviews would have been caught immediately with unit tests.
    • Fix: Add apps/web/hooks/use-multi-select.test.ts using renderHook. Cover toggle, range, stale-anchor fallback, selectAll/clearSelection, stale-path pruning, and plain-click behavior. Use /tdd.

Suggestion (recommended, doesn't block)

  1. Comment/implementation mismatch in bulk delete rollbackapps/web/components/task/file-context-menu.tsx:128 → comment

    • The comment says "re-insert only the failed node instead of rolling back everything" but each individual .catch(() => setTree(snapshot)) still restores the full pre-deletion tree, including already-successfully-deleted files. Either update the comment or switch to a single Promise.all that only restores on full batch failure.
  2. Duplicated helpers across E2E specsapps/web/e2e/tests/file-tree-multi-select.spec.ts:11 → comment

    • GitHelper, makeGitEnv, openTaskSession, createStandardProfile are copy-pasted verbatim in both spec files. Extract to apps/web/e2e/helpers/git-helper.ts.

What's fixed vs. prior reviews

  • Bulk discard without confirmation (previously P1) — Fixed. changes-panel.tsx:633 passes localDialogs.handleBulkDiscardClick as onBulkDiscard, routing through DiscardDialog before any git.discard() fires.
  • Nested isSelected propagation (previously P1) — Fixed. TreeNodeItemProps.isSelectedFn is (path: string) => boolean, evaluated per-node at TreeNodeItem:174. TreeNodeChildren's spread passes the function unchanged.
  • Blank filename for single-file bulk discard — Fixed via displayFile fallback at changes-panel-dialogs.tsx:48.
  • onSelectionChange inside state updater — Fixed. computeNextSelection is now a pure function called outside the updater; setRawSelection(next) and onSelectionChange?.(next) called sequentially.
  • Keyboard listener churn — Fixed. useKeyboardShortcuts takes clearSelection/selectAll as stable deps, not the multiSelect object.
  • Unnecessary useMemo in BulkActionBar — Fixed. Replaced with plain const paths = [...selectedPaths].
  • Missing file-tree-utils.ts unit tests — Fixed. file-tree-utils.test.ts covers computeMoveTargets (5 cases), moveNodesInTree (2 cases), getVisiblePaths (2 cases).
  • Bulk delete only deleting context-menu target — Fixed. selectedPaths iterated in handleConfirmDelete when isBulk.
  • Duplicate basename collision in moveNodesInTree — Fixed. deduplicateName() applied in both computeMoveTargets and moveNodesInTree.
  • Ghost-restore on partial bulk delete — Optimistic removal now batched in a single setTree call before the API loop.

Summary

Severity Count
Blocker 1
Suggestion 2

Verdict: Blocked — add unit tests for useMultiSelect before merge

Comment thread apps/web/hooks/use-multi-select.ts
Comment thread apps/web/components/task/file-context-menu.tsx Outdated
Comment thread apps/web/e2e/tests/file-tree-multi-select.spec.ts Outdated
- Add unit tests for computeNextSelection (8 tests covering ctrl-click,
  shift-click range, stale anchor, reverse range, shift+ctrl extend)
- Extract shared E2E helpers (GitHelper, makeGitEnv, openTaskSession,
  createStandardProfile) to e2e/helpers/git-helper.ts
- Fix misleading comment in bulk delete (says "re-insert only failed"
  but implementation restores full snapshot)
Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

zeval has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@claude
Copy link
Copy Markdown

claude bot commented Apr 11, 2026

Claude finished @zeval's task in 5m 1s —— View job


PR Review

  • Read skill instructions
  • Gather context & read changed files
  • Analyze code
  • Post inline comments
  • Post findings report

Findings

Blocker (must fix before merge)

  1. Hook-level behaviors untested in useMultiSelectapps/web/hooks/use-multi-select.test.ts:117 → comment
    • Issue: The test file covers computeNextSelection (the exported pure function) but not the hook's own behaviors. Untested: (a) stale-path pruning — when items shrinks, dropped paths are removed from selectedPaths via useMemo (lines 69–84); (b) plain click returns false and clears selection (lines 100–108); (c) selectAll and clearSelection semantics.
    • Why: Per CLAUDE.md, hooks require *.test.ts coverage. The stale-path pruning drove multiple bug-fix cycles in this PR and would have been caught earlier with a regression test.
    • Fix: Add renderHook-based tests. Use /tdd.

Suggestion (recommended, doesn't block)

  1. Ghost-restore on partial bulk delete failureapps/web/components/task/file-context-menu.tsx:136
    • Per-item .catch(() => setTree(snapshot)) restores the full pre-deletion tree on any single failure, including already-successfully-deleted files. The comment is accurate but the behavior is surprising. Fix with a single Promise.all like executeMoveFiles in file-browser.tsx.

What's fixed vs. prior reviews

  • ✅ Bulk discard without confirmation — wired to localDialogs.handleBulkDiscardClickDiscardDialog
  • ✅ Nested isSelected propagation — isSelectedFn?: (path: string) => boolean evaluated per-node
  • ✅ Blank filename for single-file bulk discard — displayFile fallback at changes-panel-dialogs.tsx:48
  • onSelectionChange inside state updater — moved outside setRawSelection
  • ✅ Keyboard listener churn — useKeyboardShortcuts takes stable clearSelection/selectAll deps
  • ✅ Unnecessary useMemo in BulkActionBar — replaced with plain spread
  • ✅ Missing file-tree-utils.ts unit tests — file-tree-utils.test.ts added (9 cases)
  • ✅ Bulk delete only deleting context-menu target — selectedPaths batched and iterated
  • ✅ Duplicate basename collision — deduplicateName() consistent across tree and API calls
  • ✅ Duplicate E2E helpers — extracted to apps/web/e2e/helpers/git-helper.ts

Summary

Severity Count
Blocker 1
Suggestion 1

Verdict: Blocked — add renderHook tests for useMultiSelect hook behaviors, then ready to merge.

Comment thread apps/web/hooks/use-multi-select.test.ts
@claude
Copy link
Copy Markdown

claude bot commented Apr 11, 2026

Findings

Blocker (must fix before merge)

  1. Hook-level behaviors untested in useMultiSelectapps/web/hooks/use-multi-select.test.ts:117 → comment
    • Issue: The test file covers computeNextSelection (the exported pure function) but not the hook's own behaviors. Three behaviors are untested: (a) stale-path pruning — when items shrinks, paths removed from items are dropped from selectedPaths via useMemo (lines 69–84); (b) plain click returns false and clears selection without consuming the event (lines 100–108); (c) selectAll and clearSelection semantics.
    • Why: Per CLAUDE.md, hooks require *.test.ts coverage. The stale-path pruning in particular drove multiple bug-fix cycles across this PR's history — it would have been caught earlier with a regression test. computeNextSelection tests don't exercise any of these.
    • Fix: Add renderHook-based tests for the three behaviors listed in the inline comment. Use /tdd.

Suggestion (recommended, doesn't block)

  1. Ghost-restore on partial bulk delete failureapps/web/components/task/file-context-menu.tsx:136
    • Each onDeleteFile(p).catch(() => setTree(snapshot)) shares the same pre-deletion snapshot. If file-a.ts deletes OK on the backend but file-b.ts fails, file-b.ts's catch restores the full original tree — including file-a.ts, even though it's gone from the backend. The comment on line 128 accurately describes this ("On any failure, restore the full pre-delete tree snapshot") so at least the behavior is documented. Fix by using a single Promise.all like executeMoveFiles does in file-browser.tsx.

What's fixed vs. prior reviews

  • Bulk discard without confirmation (previously P1)onBulkDiscard now wired to localDialogs.handleBulkDiscardClickDiscardDialog in changes-panel.tsx:633
  • Nested isSelected propagation (previously P1)TreeNodeItemProps uses isSelectedFn?: (path: string) => boolean; evaluated per-node at TreeNodeItem:174; spread through TreeNodeChildren passes the function unchanged
  • Blank filename for single-file bulk discarddisplayFile fallback at changes-panel-dialogs.tsx:48 correctly shows filesToDiscard[0] when fileToDiscard is null
  • onSelectionChange inside state updater — all calls moved outside setRawSelection; computeNextSelection computed synchronously before setRawSelection(next)
  • Keyboard listener churnuseKeyboardShortcuts takes stable clearSelection/selectAll callbacks, not the multiSelect object
  • Unnecessary useMemo in BulkActionBar — replaced with const paths = [...selectedPaths]
  • Missing file-tree-utils.ts unit testsfile-tree-utils.test.ts added with 9 cases covering computeMoveTargets, moveNodesInTree, getVisiblePaths
  • Bulk delete only deleting context-menu targetFileContextMenu accepts selectedPaths?: Set<string> and batches removal then iterates API calls
  • Duplicate basename collision in moveNodesInTreededuplicateName() added; consistent across computeMoveTargets and moveNodesInTree
  • Duplicate helpers across E2E specs — extracted to apps/web/e2e/helpers/git-helper.ts

Summary

Severity Count
Blocker 1
Suggestion 1

Verdict: Blocked — add renderHook tests for useMultiSelect hook behaviors (stale-path pruning, plain click, selectAll/clearSelection), then this is ready to merge.

@zeval
Copy link
Copy Markdown
Contributor Author

zeval commented Apr 11, 2026

@claude reevaluate your comment based on the replies

@claude
Copy link
Copy Markdown

claude bot commented Apr 11, 2026

Claude finished @zeval's task in 2m 43s —— View job


PR Review Reevaluation

  • Gather context
  • Read current state of key files
  • Assess what's fixed vs. remaining
  • Post updated findings

Findings

Remaining Concern

  1. Hook-level behaviors not yet tested in use-multi-select.test.tsapps/web/hooks/use-multi-select.ts
    • The test file (+117 lines) imports and tests computeNextSelection — the pure function explicitly marked /** Exported for testing. */. This covers the selection algorithm well (7 cases: ctrl toggle, accumulate, range, no-anchor, stale anchor, shift+ctrl, reverse range).
    • What's not tested: the hook's own reactive behaviors — (a) stale-path pruning via useMemo (lines 69–84): when items shrinks, dropped paths are removed from selectedPaths; (b) plain click: returns false, clears selection if non-empty; (c) selectAll / clearSelection.
    • Stale-path pruning in particular was a source of bugs in earlier rounds of this PR. Adding a renderHook test for it would prevent regressions.
    • This is no longer a hard blocker given the core logic is well-tested; adding the hook-level tests would bring coverage in line with the full intent of CLAUDE.md.

Suggestion (unchanged)

  1. Comment/implementation mismatch in bulk delete rollbackapps/web/components/task/file-context-menu.tsx:136
    • Comment at line 128 says "On any failure, restore the full pre-delete tree snapshot." — behavior is documented, but each individual onDeleteFile(p).catch(() => setTree(snapshot)) call can restore already-successfully-deleted files if a later delete fails. Consistent with the documented intent; use a single Promise.all to fix if desired.

What's confirmed fixed ✅

Issue Fix
Nested isSelected propagation (P1) isSelectedFn?: (path: string) => boolean evaluated per-node
Bulk discard without confirmation (P1) Routes through handleBulkDiscardClickDiscardDialog
Blank filename for single-file bulk discard displayFile fallback at changes-panel-dialogs.tsx:48isBulk uses > 1 but displayFile = filesToDiscard[0] handles the single-file case correctly
onSelectionChange inside state updater Moved outside setRawSelection — lines 106, 120, 129, 134
Keyboard listener re-registers every render useKeyboardShortcuts takes clearSelection/selectAll as stable deps (line 329, 372)
Unnecessary useMemo in BulkActionBar Replaced with const paths = [...selectedPaths]
Missing file-tree-utils.ts unit tests file-tree-utils.test.ts added (9 cases)
Bulk delete only deleting context-menu target selectedPaths iterated in handleConfirmDelete
Duplicate basename collision in moveNodesInTree deduplicateName() consistent across tree + API
index.lock force-deletion in E2E retry Pure wait-and-retry
Duplicate helpers across E2E specs Extracted to apps/web/e2e/helpers/git-helper.ts
Optimistic bulk delete ghost-restore Removal now batched in single setTree call before API loop

Summary

Severity Count
Remaining concern 1
Suggestion 1

Verdict: Near ready. The remaining gap is hook-level test coverage for useMultiSelect's reactive behaviors (stale-path pruning, selectAll, clearSelection, plain click). The core selection algorithm is well-tested via computeNextSelection. All previously flagged blockers have been addressed.

Individual .catch() handlers could restore already-deleted files
if a later delete failed. Now uses Promise.all so rollback only
happens if any delete rejects.
Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

zeval has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@claude
Copy link
Copy Markdown

claude bot commented Apr 11, 2026

Claude finished @zeval's task in 3m 59s —— View job


PR Review

  • Read skill instructions
  • Gather context & read changed files
  • Analyze code
  • Post inline comments
  • Post findings report

Findings

Blocker (must fix before merge)

  1. Hook-level behaviors untested in useMultiSelectapps/web/hooks/use-multi-select.test.ts:117 → comment
    • Issue: The test file tests computeNextSelection (the exported pure function) but not the hook's own reactive behaviors: (a) stale-path pruning — when items shrinks, dropped paths are removed from selectedPaths via useMemo (lines 69–84); (b) plain click — handleClick without a modifier returns false and clears non-empty selection (lines 99–108, entirely separate from computeNextSelection); (c) selectAll / clearSelection — new exported behaviors with no coverage.
    • Why: Per CLAUDE.md, hooks require *.test.ts coverage. The stale-path pruning is non-trivial reactive logic that cannot be exercised through the pure-function tests and has caused bugs in this PR's review cycle.
    • Fix: Add renderHook-based tests for these three behaviors. Code snippet in the inline comment. Use /tdd.

What's fixed vs. prior reviews

  • Nested isSelected propagation (previously P1)isSelectedFn?: (path: string) => boolean evaluated per-node in TreeNodeItem
  • Bulk discard without confirmation (previously P1)onBulkDiscard wired to localDialogs.handleBulkDiscardClickDiscardDialog
  • Blank filename for single-file bulk discarddisplayFile fallback at changes-panel-dialogs.tsx:48 correctly shows filesToDiscard[0] when fileToDiscard is null
  • onSelectionChange inside state updater — moved outside setRawSelection
  • Keyboard listener re-registration on every renderuseKeyboardShortcuts now takes stable clearSelection/selectAll callbacks as deps (line 329)
  • Unnecessary useMemo in BulkActionBar — replaced with plain const paths = [...selectedPaths]
  • Missing file-tree-utils.ts unit testsfile-tree-utils.test.ts added with 9 cases covering computeMoveTargets, moveNodesInTree, getVisiblePaths
  • Bulk delete only deleting context-menu targetFileContextMenu iterates selectedPaths when isBulk
  • Duplicate basename collision in moveNodesInTreededuplicateName() consistent across tree update and API calls
  • index.lock force-deletion in E2E retry — pure wait-and-retry
  • Duplicate helpers across E2E specs — extracted to apps/web/e2e/helpers/git-helper.ts
  • Ghost-restore on partial bulk delete failurePromise.all in latest commit; rollback only happens when any delete rejects

Summary

Severity Count
Blocker 1

Verdict: Blocked — add renderHook tests for useMultiSelect hook behaviors (stale-path pruning, plain click, selectAll/clearSelection), then this is ready to merge.

Comment thread apps/web/hooks/use-multi-select.test.ts
- Stale-path pruning: items shrink → selected paths auto-pruned
- Plain click: returns false, clears existing selection
- selectAll/clearSelection: work correctly, fire onSelectionChange
- Install @testing-library/react for renderHook support
Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

zeval has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@claude
Copy link
Copy Markdown

claude bot commented Apr 11, 2026

Claude finished @zeval's task in 3m 47s —— View job


PR Review

  • Read skill instructions
  • Read current state of key files
  • Analyze changes since last review
  • Post inline comments
  • Post findings report

Findings

Suggestion (recommended, doesn't block)

  1. Bulk delete rollback only fires on rejection, not false returnsapps/web/components/task/file-context-menu.tsx:136 → comment
    • Single-file deleteNodeOptimistically handles both !ok and throws; the bulk Promise.all(...).catch(...) only handles throws. If onDeleteFile returns false, the optimistic removal stays and the UI diverges from the backend.

What's fixed

  • renderHook tests for useMultiSelect — stale-path pruning, plain click, selectAll/clearSelection, onSelectionChange callback
  • Bulk discard without confirmation — routes through handleBulkDiscardClickDiscardDialog
  • Nested isSelected propagationisSelectedFn evaluated per-node
  • Blank filename for single-file bulk discarddisplayFile fallback
  • onSelectionChange inside state updater — moved outside setRawSelection
  • Keyboard listener churn — stable clearSelection/selectAll deps
  • Ghost-restore on partial bulk deletePromise.all with batched optimistic removal
  • Missing file-tree-utils.ts unit tests — 9 cases added
  • Duplicate basename collisiondeduplicateName()
  • Duplicate E2E helpers — extracted to git-helper.ts

Summary

Severity Count
Blocker 0
Suggestion 1

Verdict: Ready to merge — all blockers resolved. One minor suggestion on bulk delete rollback path.

Comment thread apps/web/components/task/file-context-menu.tsx Outdated
@claude
Copy link
Copy Markdown

claude bot commented Apr 11, 2026

Findings

Suggestion (recommended, doesn't block)

  1. Bulk delete rollback only fires on rejection, not on false returns — `apps/web/components/task/file-context-menu.tsx:136` → comment
    • The single-file deleteNodeOptimistically handles both !ok returns and rejections. The bulk path only has .catch() — if onDeleteFile returns false without throwing, the optimistic removal isn't reverted and the UI diverges from backend state.
    • Fix: add .then(results => { if (results.some(ok => !ok)) setTree(snapshot); }) before the .catch().

What's fixed vs. prior reviews

  • renderHook tests for useMultiSelect hook behaviorsuse-multi-select.test.ts now covers stale-path pruning (useMemo when items shrink), plain click (returns false, clears selection), selectAll / clearSelection, and onSelectionChange callback firing
  • Bulk discard without confirmation — routes through handleBulkDiscardClickDiscardDialog
  • Nested isSelected propagationisSelectedFn?: (path: string) => boolean evaluated per-node in TreeNodeItem
  • Blank filename for single-file bulk discarddisplayFile fallback at changes-panel-dialogs.tsx:48 correctly shows filesToDiscard[0]
  • onSelectionChange inside state updater — all calls moved outside setRawSelection
  • Keyboard listener churnuseKeyboardShortcuts takes stable clearSelection/selectAll callbacks
  • Ghost-restore on partial bulk deletePromise.all with batched optimistic removal
  • Missing file-tree-utils.ts unit tests — 9 cases covering computeMoveTargets, moveNodesInTree, getVisiblePaths
  • Duplicate basename collisiondeduplicateName() consistent across tree update and API
  • Bulk delete only deleting context-menu targetselectedPaths iterated in handleConfirmDelete
  • Duplicate E2E helpers — extracted to apps/web/e2e/helpers/git-helper.ts

Summary

Severity Count
Blocker 0
Suggestion 1

Verdict: Ready to merge — all previously flagged blockers are resolved. One remaining suggestion (false-return rollback in bulk delete) is non-blocking.

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

zeval has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@claude
Copy link
Copy Markdown

claude bot commented Apr 11, 2026

Claude finished @zeval's task in 4m 46s —— View job


PR Review

  • Read skill instructions
  • Gather context & read changed files
  • Analyze code
  • Post inline comments
  • Post findings report

Findings

Suggestion (recommended, doesn't block)

  1. Unused dev dependency @testing-library/react-hooksapps/web/package.json:123 → comment
    • use-multi-select.test.ts imports renderHook from @testing-library/react (line 2), not from @testing-library/react-hooks. The latter package is never imported anywhere in the codebase.
    • @testing-library/react-hooks was made redundant when @testing-library/react v13 added renderHook directly. At v16 it's doubly unnecessary.
    • Remove "@testing-library/react-hooks": "^8.0.1" from package.json and clean up pnpm-lock.yaml.

What's fixed vs. prior reviews

Issue Status
Nested isSelected propagation (P1) ✅ Fixed — isSelectedFn?: (path: string) => boolean evaluated per-node in TreeNodeItem
Bulk discard without confirmation (P1) ✅ Fixed — onBulkDiscard routes through handleBulkDiscardClickDiscardDialog
Blank filename for single-file bulk discard ✅ Fixed — displayFile fallback at changes-panel-dialogs.tsx:48 shows filesToDiscard[0]
onSelectionChange inside state updater ✅ Fixed — called outside setRawSelection
Keyboard listener re-registers every render ✅ Fixed — useKeyboardShortcuts takes stable clearSelection/selectAll deps
Unnecessary useMemo in BulkActionBar ✅ Fixed — plain const paths = [...selectedPaths]
Missing file-tree-utils.ts unit tests ✅ Fixed — 9 cases covering computeMoveTargets, moveNodesInTree, getVisiblePaths
Missing useMultiSelect unit tests ✅ Fixed — renderHook tests for stale-path pruning, plain click, selectAll/clearSelection
Bulk delete only deleting context-menu target ✅ Fixed — selectedPaths iterated in handleConfirmDelete
Duplicate basename collision in moveNodesInTree ✅ Fixed — deduplicateName() consistent across tree update and API calls
Ghost-restore on partial bulk delete ✅ Fixed — Promise.all with batched optimistic removal
Bulk delete rollback only on rejection, not false returns ✅ Fixed (commit 23be892) — .then(results => { if (results.some(ok => !ok)) setTree(snapshot); })
Duplicate helpers across E2E specs ✅ Fixed — extracted to apps/web/e2e/helpers/git-helper.ts

Summary

Severity Count
Blocker 0
Suggestion 1

Verdict: Ready to merge — all previously flagged blockers are resolved. One minor unused dependency to clean up if desired.

Comment thread apps/web/package.json Outdated
"@playwright/test": "^1.58.2",
"@tailwindcss/postcss": "^4",
"@testing-library/react": "^16.3.2",
"@testing-library/react-hooks": "^8.0.1",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unused dev dependency@testing-library/react-hooks@8.0.1 was added but is never imported anywhere. renderHook comes from @testing-library/react directly (line 2 of use-multi-select.test.ts). The react-hooks package was superseded by the built-in renderHook in @testing-library/react v13+.

Remove it:

Suggested change
"@testing-library/react-hooks": "^8.0.1",

And remove its entry from pnpm-lock.yaml.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3df81c0 — removed @testing-library/react-hooks. renderHook comes from @testing-library/react directly.

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

zeval has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@claude
Copy link
Copy Markdown

claude bot commented Apr 11, 2026

Claude finished @zeval's task in 4m 2s —— View job


PR Review

  • Read skill instructions
  • Gather context & read changed files
  • Analyze code
  • Post inline comments (none — no new issues)
  • Post findings report

Findings

What's fixed vs. prior reviews

  • Bulk discard without confirmation (previously P1)onBulkDiscard wired to localDialogs.handleBulkDiscardClickDiscardDialog in changes-panel.tsx:633
  • Nested isSelected propagation (previously P1)isSelectedFn?: (path: string) => boolean evaluated per-node in TreeNodeItem:174; spread through TreeNodeChildren unchanged
  • renderHook tests for useMultiSelect — covers stale-path pruning, plain click, selectAll/clearSelection, onSelectionChange callback
  • Missing file-tree-utils.ts unit testsfile-tree-utils.test.ts with 9 cases: computeMoveTargets, moveNodesInTree, getVisiblePaths
  • Bulk delete rollback on false returns.then(results => { if (results.some(ok => !ok)) setTree(snapshot); }) added in 23be892
  • Unused @testing-library/react-hooks — Removed in 3df81c0
  • onSelectionChange inside state updater — Moved outside setRawSelection
  • Keyboard listener churnuseKeyboardShortcuts takes stable clearSelection/selectAll as deps
  • Unnecessary useMemo in BulkActionBar — Replaced with const paths = [...selectedPaths]
  • Duplicate basename collision in moveNodesInTreededuplicateName() consistent across tree and API
  • Bulk delete only deleting context-menu targetselectedPaths iterated in handleConfirmDelete
  • Duplicate helpers across E2E specs — Extracted to apps/web/e2e/helpers/git-helper.ts

Summary

Severity Count
Blocker 0
Suggestion 0

Verdict: Ready to merge — every blocker from prior review cycles has been resolved. The last outstanding suggestion (unused @testing-library/react-hooks) was removed in 3df81c0. No new issues introduced.

@claude
Copy link
Copy Markdown

claude bot commented Apr 11, 2026

Findings

What's fixed vs. prior reviews

  • Bulk discard without confirmation (previously P1)onBulkDiscard is wired to localDialogs.handleBulkDiscardClickDiscardDialog in changes-panel.tsx:633
  • Nested isSelected propagation (previously P1)TreeNodeItemProps uses isSelectedFn?: (path: string) => boolean, evaluated per-node in TreeNodeItem:174; spread through TreeNodeChildren passes the function unchanged
  • renderHook tests for useMultiSelect hook behaviorsuse-multi-select.test.ts covers stale-path pruning, plain click (returns false, clears selection), selectAll/clearSelection, and onSelectionChange callback
  • Missing file-tree-utils.ts unit testsfile-tree-utils.test.ts covers computeMoveTargets (5 cases), moveNodesInTree (2 cases), getVisiblePaths (2 cases)
  • Bulk delete rollback only fires on rejection, not false returns.then(results => { if (results.some(ok => !ok)) setTree(snapshot); }) added in commit 23be892
  • Unused @testing-library/react-hooks dependency — Removed in commit 3df81c0
  • onSelectionChange called inside state updater — Moved outside setRawSelection throughout use-multi-select.ts
  • Keyboard listener churnuseKeyboardShortcuts takes stable clearSelection/selectAll as deps, not the multiSelect object
  • Unnecessary useMemo in BulkActionBar — Replaced with plain const paths = [...selectedPaths]
  • Duplicate basename collision in moveNodesInTreededuplicateName() consistent across tree update and API calls
  • Bulk delete only deleting context-menu targetFileContextMenu iterates selectedPaths when isBulk
  • Duplicate helpers across E2E specs — Extracted to apps/web/e2e/helpers/git-helper.ts

Summary

Severity Count
Blocker 0
Suggestion 0

Verdict: Ready to merge — all blockers from prior reviews are resolved. The unused dependency flagged in the last review was cleaned up in 3df81c0. No new issues introduced.

@zeval zeval merged commit 2b8513f into main Apr 11, 2026
26 checks passed
@zeval zeval deleted the feature/multi-entity-selecti-eyb branch April 11, 2026 16:43
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