Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,7 @@ function MaestroConsoleInner() {
handleFileClick,
updateSessionWorkingDirectory,
toggleFolder,
toggleFolderRecursive,
expandAllFolders,
collapseAllFolders,
} = useAppHandlers({
Expand Down Expand Up @@ -2374,6 +2375,7 @@ function MaestroConsoleInner() {

// File explorer handlers
toggleFolder,
toggleFolderRecursive,
handleFileClick,
expandAllFolders,
collapseAllFolders,
Expand Down
15 changes: 13 additions & 2 deletions src/renderer/components/FileExplorerPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,11 @@ interface FileExplorerPanelProps {
activeSessionId: string,
setSessions: React.Dispatch<React.SetStateAction<Session[]>>
) => void;
toggleFolderRecursive: (
path: string,
activeSessionId: string,
setSessions: React.Dispatch<React.SetStateAction<Session[]>>
) => void;
handleFileClick: (node: any, path: string, activeSession: Session) => Promise<void>;
expandAllFolders: (
activeSessionId: string,
Expand Down Expand Up @@ -387,6 +392,7 @@ function FileExplorerPanelInner(props: FileExplorerPanelProps) {
setActiveFocus,
fileTreeFilterInputRef,
toggleFolder,
toggleFolderRecursive,
handleFileClick,
expandAllFolders,
collapseAllFolders,
Expand Down Expand Up @@ -998,9 +1004,13 @@ function FileExplorerPanelInner(props: FileExplorerPanelProps) {
e.preventDefault();
}
}}
onClick={() => {
onClick={(e) => {
if (isFolder) {
toggleFolder(fullPath, session.id, setSessions);
if (e.altKey) {
toggleFolderRecursive(fullPath, session.id, setSessions);
} else {
toggleFolder(fullPath, session.id, setSessions);
}
} else {
setSelectedFileIndex(globalIndex);
// Only change focus if not filtering
Expand Down Expand Up @@ -1073,6 +1083,7 @@ function FileExplorerPanelInner(props: FileExplorerPanelProps) {
selectedFileIndex,
theme,
toggleFolder,
toggleFolderRecursive,
setSessions,
setSelectedFileIndex,
setActiveFocus,
Expand Down
7 changes: 7 additions & 0 deletions src/renderer/components/RightPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ interface RightPanelProps {
activeSessionId: string,
setSessions: React.Dispatch<React.SetStateAction<Session[]>>
) => void;
toggleFolderRecursive: (
path: string,
activeSessionId: string,
setSessions: React.Dispatch<React.SetStateAction<Session[]>>
) => void;
handleFileClick: (node: any, path: string, activeSession: Session) => Promise<void>;
expandAllFolders: (
activeSessionId: string,
Expand Down Expand Up @@ -165,6 +170,7 @@ export const RightPanel = memo(
fileTreeContainerRef,
fileTreeFilterInputRef,
toggleFolder,
toggleFolderRecursive,
handleFileClick,
expandAllFolders,
collapseAllFolders,
Expand Down Expand Up @@ -515,6 +521,7 @@ export const RightPanel = memo(
setActiveFocus={setActiveFocus}
fileTreeFilterInputRef={fileTreeFilterInputRef}
toggleFolder={toggleFolder}
toggleFolderRecursive={toggleFolderRecursive}
handleFileClick={handleFileClick}
expandAllFolders={expandAllFolders}
collapseAllFolders={collapseAllFolders}
Expand Down
7 changes: 7 additions & 0 deletions src/renderer/hooks/props/useRightPanelProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export interface UseRightPanelPropsDeps {
activeSessionId: string,
setSessions: React.Dispatch<React.SetStateAction<Session[]>>
) => void;
toggleFolderRecursive: (
path: string,
activeSessionId: string,
setSessions: React.Dispatch<React.SetStateAction<Session[]>>
) => void;
handleFileClick: (node: any, path: string, activeSession: Session) => Promise<void>;
expandAllFolders: (
activeSessionId: string,
Expand Down Expand Up @@ -107,6 +112,7 @@ export function useRightPanelProps(deps: UseRightPanelPropsDeps) {

// File explorer handlers
toggleFolder: deps.toggleFolder,
toggleFolderRecursive: deps.toggleFolderRecursive,
handleFileClick: deps.handleFileClick,
expandAllFolders: deps.expandAllFolders,
collapseAllFolders: deps.collapseAllFolders,
Expand Down Expand Up @@ -154,6 +160,7 @@ export function useRightPanelProps(deps: UseRightPanelPropsDeps) {
// Stable callbacks
deps.handleSetActiveRightTab,
deps.toggleFolder,
deps.toggleFolderRecursive,
deps.handleFileClick,
deps.expandAllFolders,
deps.collapseAllFolders,
Expand Down
88 changes: 87 additions & 1 deletion src/renderer/hooks/ui/useAppHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type { Session, FocusArea } from '../../types';
import { shouldOpenExternally, getAllFolderPaths } from '../../utils/fileExplorer';
import {
shouldOpenExternally,
getAllFolderPaths,
type FileTreeNode,
} from '../../utils/fileExplorer';
import { useModalStore } from '../../stores/modalStore';
import { useFileExplorerStore } from '../../stores/fileExplorerStore';

Expand Down Expand Up @@ -74,6 +78,12 @@ export interface UseAppHandlersReturn {
sessionId: string,
setSessions: React.Dispatch<React.SetStateAction<Session[]>>
) => void;
/** Toggle folder and all descendants (Alt+Click) */
toggleFolderRecursive: (
path: string,
sessionId: string,
setSessions: React.Dispatch<React.SetStateAction<Session[]>>
) => void;
/** Expand all folders in file tree */
expandAllFolders: (
sessionId: string,
Expand All @@ -87,6 +97,45 @@ export interface UseAppHandlersReturn {
) => void;
}

/**
* Collect all descendant folder paths under a given set of nodes.
*/
function collectDescendantFolders(nodes: FileTreeNode[], 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 a folder by targetPath in the tree and return it plus all descendant folder paths.
*/
function findSubtreeFolders(
nodes: FileTreeNode[],
currentPath: string,
targetPath: string
): string[] | null {
for (const node of nodes) {
const fullPath = currentPath ? `${currentPath}/${node.name}` : node.name;
if (fullPath === targetPath && node.type === 'folder') {
const descendants = node.children ? collectDescendantFolders(node.children, fullPath) : [];
return [fullPath, ...descendants];
}
if (node.type === 'folder' && node.children && targetPath.startsWith(fullPath + '/')) {
const found = findSubtreeFolders(node.children, fullPath, targetPath);
if (found) return found;
}
}
return null;
}

/**
* Hook for app-level handlers: drag events, file operations, and folder management.
*
Expand Down Expand Up @@ -301,6 +350,42 @@ export function useAppHandlers(deps: UseAppHandlersDeps): UseAppHandlersReturn {
[]
);

/**
* Toggle a folder and all its descendant folders (Alt+Click behavior).
* If the folder is currently expanded, collapse it and all descendants.
* If collapsed, expand it and all descendants.
*/
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);
const allPaths = findSubtreeFolders(s.fileTree, '', path) || [path];

if (isCurrentlyExpanded) {
for (const p of allPaths) {
expanded.delete(p);
}
} else {
for (const p of allPaths) {
expanded.add(p);
}
}

return { ...s, fileExplorerExpanded: Array.from(expanded) };
})
);
},
[]
);
Comment on lines +358 to +387
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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!


const expandAllFolders = useCallback(
(
sessionId: string,
Expand Down Expand Up @@ -346,6 +431,7 @@ export function useAppHandlers(deps: UseAppHandlersDeps): UseAppHandlersReturn {

// Folder handlers
toggleFolder,
toggleFolderRecursive,
expandAllFolders,
collapseAllFolders,
};
Expand Down