feat: Enable alt/option + click to expand and collapse subdirectories recursively in the file browser#738
Conversation
📝 WalkthroughWalkthroughAdded a recursive folder-toggle handler and threaded it through the UI: Alt+clicking a folder now calls Changes
Sequence DiagramsequenceDiagram
participant User as User
participant FE as FileExplorerPanel
participant RP as RightPanel
participant Hook as useAppHandlers
participant State as FileTreeState
User->>FE: Click folder (Alt key)
FE->>FE: detect e.altKey = true
FE->>RP: toggleFolderRecursive(path, sessionId, setSessions)
RP->>Hook: forward toggleFolderRecursive(...)
Hook->>State: traverse fileTree, collect descendant folder paths
Hook->>State: update fileExplorerExpanded with collected paths
State-->>Hook: updated sessions/state
Hook-->>FE: sessions/state changed (re-render)
FE-->>User: folder and descendants expanded/collapsed
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~22 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds alt/option+click support to recursively expand or collapse subdirectories in the file browser. When a user holds Alt and clicks a folder, the target folder and all of its descendant folders are toggled together (expand all or collapse all), complementing the existing single-folder toggle and the global expand/collapse-all buttons. The implementation is clean and consistent with existing patterns:
Two minor style suggestions:
Confidence Score: 4/5Safe to merge — the new feature is additive and does not affect existing click behaviour. The recursive tree traversal logic is correct; expand and collapse paths are both handled; the fallback is sensible; and the prop-drilling chain is complete. Minor concerns are limited to UX discoverability and a cosmetic code-structure point, neither of which affects correctness. No files require special attention beyond the style suggestions already noted. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User Alt+Clicks a folder] --> B{isFolder && e.altKey?}
B -- Yes --> C[toggleFolderRecursive called]
B -- No --> D[toggleFolder called — single toggle]
C --> E[findSubtreeFolders traverses file tree]
E --> F{Target node found?}
F -- Yes --> G[Collect target + all descendant folder paths]
F -- No --> H[Fallback: use only the clicked path]
G --> I{Currently expanded?}
H --> I
I -- Yes --> J[Delete all collected paths from expanded set — collapse]
I -- No --> K[Add all collected paths to expanded set — expand]
J --> L[Update session state]
K --> L
L --> M[File tree re-renders with new expansion state]
|
| const toggleFolderRecursive = useCallback( | ||
| ( | ||
| path: string, | ||
| sessionId: string, | ||
| setSessionsFn: React.Dispatch<React.SetStateAction<Session[]>> | ||
| ) => { | ||
| setSessionsFn((prev) => | ||
| prev.map((s) => { | ||
| if (s.id !== sessionId) return s; | ||
| if (!s.fileExplorerExpanded || !s.fileTree) return s; | ||
| const expanded = new Set(s.fileExplorerExpanded); | ||
| const isCurrentlyExpanded = expanded.has(path); | ||
|
|
||
| // Find the node at this path and collect all descendant folder paths | ||
| const collectDescendantFolders = ( | ||
| nodes: typeof s.fileTree, | ||
| currentPath: string | ||
| ): string[] => { | ||
| const result: string[] = []; | ||
| for (const node of nodes!) { | ||
| const fullPath = currentPath ? `${currentPath}/${node.name}` : node.name; | ||
| if (node.type === 'folder') { | ||
| result.push(fullPath); | ||
| if (node.children) { | ||
| result.push(...collectDescendantFolders(node.children, fullPath)); | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
| }; | ||
|
|
||
| // Find the subtree starting at the target path | ||
| const findSubtreeFolders = ( | ||
| nodes: typeof s.fileTree, | ||
| currentPath: string | ||
| ): string[] | null => { | ||
| for (const node of nodes!) { | ||
| const fullPath = currentPath ? `${currentPath}/${node.name}` : node.name; | ||
| if (fullPath === path && node.type === 'folder') { | ||
| // Found the target - collect all descendants | ||
| const descendants = node.children | ||
| ? collectDescendantFolders(node.children, fullPath) | ||
| : []; | ||
| return [fullPath, ...descendants]; | ||
| } | ||
| if (node.type === 'folder' && node.children && path.startsWith(fullPath + '/')) { | ||
| // Recurse into this folder | ||
| const found = findSubtreeFolders(node.children, fullPath); | ||
| if (found) return found; | ||
| } | ||
| } | ||
| return null; | ||
| }; | ||
|
|
||
| const allPaths = findSubtreeFolders(s.fileTree, '') || [path]; | ||
|
|
||
| if (isCurrentlyExpanded) { | ||
| // Collapse: remove the folder and all descendants | ||
| for (const p of allPaths) { | ||
| expanded.delete(p); | ||
| } | ||
| } else { | ||
| // Expand: add the folder and all descendants | ||
| for (const p of allPaths) { | ||
| expanded.add(p); | ||
| } | ||
| } | ||
|
|
||
| return { ...s, fileExplorerExpanded: Array.from(expanded) }; | ||
| }) | ||
| ); | ||
| }, | ||
| [] | ||
| ); |
There was a problem hiding this comment.
Helper functions defined inside state updater
Both collectDescendantFolders and findSubtreeFolders are declared inside the setSessionsFn state-updater callback. They are recreated on every invocation of the updater. While this is functionally correct and unlikely to be a noticeable bottleneck for most file trees, it is an unusual pattern. Moving them outside the useCallback (as module-level helpers or into useCallback's body before the setSessionsFn call) would be cleaner and avoids any potential issue if the state updater were ever called in rapid succession on a very large tree.
// Suggested: lift helpers out of the updater callback
const collectDescendantFolders = (nodes: FileNode[], currentPath: string): string[] => { ... };
const findSubtreeFolders = (nodes: FileNode[], currentPath: string, targetPath: string): string[] | null => { ... };
const toggleFolderRecursive = useCallback(
(path, sessionId, setSessionsFn) => {
setSessionsFn((prev) =>
prev.map((s) => {
// ... use the lifted helpers
})
);
},
[]
);Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
Thanks for the contribution, @scriptease! This is a nice quality-of-life improvement. The implementation is clean — prop threading follows the existing Looks good to merge! 👍 |
c5c4291 to
a2c0907
Compare
… recursively in the file browser Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…scope Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add Alt+click unit test for recursive folder toggle in FileExplorerPanel. - Thread toggleFolderRecursive into existing test fixtures so new prop doesn't break RightPanel and AutoRunRightPanel integration tests. - Add title tooltip on folder rows to hint at Alt/Option+click behavior.
a2c0907 to
605ad62
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/__tests__/integration/AutoRunRightPanel.test.tsx (1)
270-291: Optional: extract a default-props factory to reduce duplication across 11 render sites.Each new required
RightPanelprop currently has to be threaded through ~11 nearly identical call sites in this file, which is what this PR illustrates. The sibling testsrc/__tests__/renderer/components/RightPanel.test.tsxalready uses acreateDefaultPropsfactory (per the cross-file context snippet) for exactly this reason. Adopting the same pattern here would make future prop additions a one-line change and reduce drift between render sites. Safe to defer.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/integration/AutoRunRightPanel.test.tsx` around lines 270 - 291, Extract a default-props factory function (e.g., createDefaultRightPanelProps) in this test file that returns the common RightPanel props object (including toggleFolderRecursive, handleFileClick, expandAllFolders, collapseAllFolders, updateSessionWorkingDirectory, refreshFileTree, setSessions, showHiddenFiles, setShowHiddenFiles, autoRunDocumentList, autoRunDocumentTree, autoRunContent, autoRunContentVersion, autoRunIsLoadingDocuments, onAutoRunContentChange, onAutoRunModeChange, onAutoRunStateChange, onAutoRunSelectDocument, onAutoRunCreateDocument, onAutoRunRefresh, onAutoRunOpenSetup), and then replace the inline prop lists at each render site with <RightPanel {...createDefaultRightPanelProps({ overrides })}>, passing only overrides for test-specific props like autoRunContent or handlers (e.g., autoRunContent, handleContentChange, handleModeChange, handleStateChange) so future prop additions are centralized and each of the ~11 render calls becomes a one-line change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/__tests__/integration/AutoRunRightPanel.test.tsx`:
- Around line 270-291: Extract a default-props factory function (e.g.,
createDefaultRightPanelProps) in this test file that returns the common
RightPanel props object (including toggleFolderRecursive, handleFileClick,
expandAllFolders, collapseAllFolders, updateSessionWorkingDirectory,
refreshFileTree, setSessions, showHiddenFiles, setShowHiddenFiles,
autoRunDocumentList, autoRunDocumentTree, autoRunContent, autoRunContentVersion,
autoRunIsLoadingDocuments, onAutoRunContentChange, onAutoRunModeChange,
onAutoRunStateChange, onAutoRunSelectDocument, onAutoRunCreateDocument,
onAutoRunRefresh, onAutoRunOpenSetup), and then replace the inline prop lists at
each render site with <RightPanel {...createDefaultRightPanelProps({ overrides
})}>, passing only overrides for test-specific props like autoRunContent or
handlers (e.g., autoRunContent, handleContentChange, handleModeChange,
handleStateChange) so future prop additions are centralized and each of the ~11
render calls becomes a one-line change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 20a764ae-afab-4ca5-bda9-73efc498348d
📒 Files selected for processing (1)
src/__tests__/integration/AutoRunRightPanel.test.tsx
|
Thanks again, @scriptease — appreciate the contribution! I rebased onto |
Just adding a small shortcut in the middle of the collapse and expand all button
Summary by CodeRabbit