From 8f1fb44b24bc1f4115efd437e428d64250126fd1 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 28 Jan 2026 09:25:38 -0800 Subject: [PATCH 1/5] Update view mode toggle --- .../ChangesView/components/ViewModeToggle/ViewModeToggle.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/ViewModeToggle/ViewModeToggle.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/ViewModeToggle/ViewModeToggle.tsx index 4b6c202e2c8..7e260f85946 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/ViewModeToggle/ViewModeToggle.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/ViewModeToggle/ViewModeToggle.tsx @@ -1,6 +1,6 @@ import { Button } from "@superset/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; -import { LuFolder, LuFolderTree } from "react-icons/lu"; +import { LuFolderTree, LuList } from "react-icons/lu"; import type { ChangesViewMode } from "../../types"; interface ViewModeToggleProps { @@ -27,7 +27,7 @@ export function ViewModeToggle({ aria-label={viewMode === "grouped" ? "Grouped view" : "Tree view"} > {viewMode === "grouped" ? ( - + ) : ( )} From f8461f888fa990979091dbc65684d3fbb263a11f Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 28 Jan 2026 09:43:39 -0800 Subject: [PATCH 2/5] Fix context menu --- .../components/FileList/FileListGrouped.tsx | 32 ++- .../components/FileList/FileListTree.tsx | 66 +++++- .../components/FolderRow/FolderRow.tsx | 194 ++++++++++++++++-- 3 files changed, 257 insertions(+), 35 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileList/FileListGrouped.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileList/FileListGrouped.tsx index 8aee451cbbc..22c7e827ecc 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileList/FileListGrouped.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileList/FileListGrouped.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useCallback, useState } from "react"; import type { ChangeCategory, ChangedFile } from "shared/changes-types"; import { FileItem } from "../FileItem"; import { FolderRow } from "../FolderRow"; @@ -88,8 +88,28 @@ function FolderGroupItem({ isExpandedView, }: FolderGroupItemProps) { const [isExpanded, setIsExpanded] = useState(true); - const isRoot = group.folderPath === ""; - const displayName = isRoot ? "Root Path" : group.folderPath; + const displayName = group.folderPath || "Root Path"; + + const handleStageAll = useCallback(() => { + if (!onStage) return; + for (const file of group.files) { + onStage(file); + } + }, [group.files, onStage]); + + const handleUnstageAll = useCallback(() => { + if (!onUnstage) return; + for (const file of group.files) { + onUnstage(file); + } + }, [group.files, onUnstage]); + + const handleDiscardAll = useCallback(() => { + if (!onDiscard) return; + for (const file of group.files) { + onDiscard(file); + } + }, [group.files, onDiscard]); return ( {group.files.map((file) => ( & { children?: Record; @@ -118,6 +134,30 @@ function TreeNodeComponent({ const isFile = node.type === "file"; const isSelected = selectedPath === node.path && !selectedCommitHash; + const handleStageAll = useCallback(() => { + if (!onStage) return; + const files = collectFilesFromNode(node); + for (const file of files) { + onStage(file); + } + }, [node, onStage]); + + const handleUnstageAll = useCallback(() => { + if (!onUnstage) return; + const files = collectFilesFromNode(node); + for (const file of files) { + onUnstage(file); + } + }, [node, onUnstage]); + + const handleDiscardAll = useCallback(() => { + if (!onDiscard) return; + const files = collectFilesFromNode(node); + for (const file of files) { + onDiscard(file); + } + }, [node, onDiscard]); + if (hasChildren) { return ( {node.children?.map((child) => ( void; children: ReactNode; - /** Number of level indentations (for tree view) */ level?: number; - /** Show file count badge */ fileCount?: number; - /** Use compact styling (grouped view) or full styling (tree view) */ variant?: "tree" | "grouped"; + folderPath?: string; + worktreePath?: string; + onStageAll?: () => void; + onUnstageAll?: () => void; + onDiscardAll?: () => void; + isActioning?: boolean; } function LevelIndicators({ level }: { level: number }) { @@ -33,14 +57,24 @@ function FolderRowHeader({ level, fileCount, isGrouped, + isExpanded, }: { name: string; level: number; fileCount?: number; isGrouped: boolean; + isExpanded: boolean; }) { return ( <> + {!isGrouped && ( + + )} {!isGrouped && }
{ + if (absolutePath) { + await navigator.clipboard.writeText(absolutePath); + } + }; + + const handleCopyRelativePath = async () => { + if (folderPath) { + await navigator.clipboard.writeText(folderPath); + } + }; + + const handleRevealInFinder = () => { + if (absolutePath) { + openInFinderMutation.mutate(absolutePath); + } + }; + + const handleOpenInApp = () => { + if (absolutePath) { + openInAppMutation.mutate({ path: absolutePath, app: lastUsedApp }); + } + }; + + const hasContextMenu = worktreePath && folderPath !== undefined; + + const triggerContent = ( + - } > - {children} - + + + ); + + const contextMenuContent = ( + + + + Copy Path + + {!isRoot && ( + + + Copy Relative Path + + )} + + + + Reveal in Finder + + + + Open in Editor + + + {(onStageAll || onUnstageAll || onDiscardAll) && ( + + )} + + {onStageAll && ( + + + Stage All + + )} + + {onUnstageAll && ( + + + Unstage All + + )} + + {onDiscardAll && ( + + + Discard All + + )} + + ); + + return ( + + {hasContextMenu ? ( + + {triggerContent} + {contextMenuContent} + + ) : ( + triggerContent + )} + + {children} + + ); } From bdc6d2bd49206571c1c90bce3977581c184080ca Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 28 Jan 2026 09:50:34 -0800 Subject: [PATCH 3/5] feat(desktop): add context menu to folders in changes sidebar - Add context menu to folder rows in tree and grouped view - Support Copy Path, Copy Relative Path, Reveal in Finder, Open in Editor - Add Stage All, Unstage All, Discard All actions for folders - Extract shared usePathActions hook for FileItem and FolderRow --- .../components/FileItem/FileItem.tsx | 46 ++++--------- .../components/FolderRow/FolderRow.tsx | 55 ++++++---------- .../Sidebar/ChangesView/hooks/index.ts | 1 + .../ChangesView/hooks/usePathActions.ts | 64 +++++++++++++++++++ 4 files changed, 96 insertions(+), 70 deletions(-) create mode 100644 apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/hooks/index.ts create mode 100644 apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/hooks/usePathActions.ts diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileItem/FileItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileItem/FileItem.tsx index 8611070e169..bef066724b6 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileItem/FileItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileItem/FileItem.tsx @@ -27,9 +27,9 @@ import { LuTrash2, LuUndo2, } from "react-icons/lu"; -import { electronTrpc } from "renderer/lib/electron-trpc"; import type { ChangeCategory, ChangedFile } from "shared/changes-types"; import { createFileKey, useScrollContext } from "../../../../ChangesContent"; +import { usePathActions } from "../../hooks"; import { getStatusColor, getStatusIndicator } from "../../utils"; interface FileItemProps { @@ -97,33 +97,14 @@ export function FileItem({ category && activeFileKey === createFileKey(file, category, commitHash); const isHighlighted = isExpandedView ? isScrollSyncActive : isSelected; - const openInFinderMutation = electronTrpc.external.openInFinder.useMutation(); - const openInEditorMutation = - electronTrpc.external.openFileInEditor.useMutation(); - const absolutePath = worktreePath ? `${worktreePath}/${file.path}` : null; - const handleCopyPath = async () => { - if (absolutePath) { - await navigator.clipboard.writeText(absolutePath); - } - }; - - const handleCopyRelativePath = async () => { - await navigator.clipboard.writeText(file.path); - }; - - const handleRevealInFinder = () => { - if (absolutePath) { - openInFinderMutation.mutate(absolutePath); - } - }; - - const handleOpenInEditor = useCallback(() => { - if (absolutePath && worktreePath) { - openInEditorMutation.mutate({ path: absolutePath, cwd: worktreePath }); - } - }, [absolutePath, worktreePath, openInEditorMutation]); + const { copyPath, copyRelativePath, revealInFinder, openInEditor } = + usePathActions({ + absolutePath, + relativePath: file.path, + cwd: worktreePath, + }); const handleClick = useCallback(() => { // Clear any pending single-click timeout @@ -149,10 +130,9 @@ export function FileItem({ clickTimeoutRef.current = null; } - // Execute double-click action (open in editor) - handleOpenInEditor(); + openInEditor(); }, - [handleOpenInEditor], + [openInEditor], ); // Cleanup timeout on unmount @@ -285,20 +265,20 @@ export function FileItem({ {fileContent} - + Copy Path - + Copy Relative Path - + Reveal in Finder - + Open in Editor diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FolderRow/FolderRow.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FolderRow/FolderRow.tsx index d9facb4b5cf..6ee483e698e 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FolderRow/FolderRow.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FolderRow/FolderRow.tsx @@ -21,7 +21,7 @@ import { LuPlus, LuUndo2, } from "react-icons/lu"; -import { electronTrpc } from "renderer/lib/electron-trpc"; +import { usePathActions } from "../../hooks"; interface FolderRowProps { name: string; @@ -114,41 +114,24 @@ export function FolderRow({ isActioning = false, }: FolderRowProps) { const isGrouped = variant === "grouped"; - const openInFinderMutation = electronTrpc.external.openInFinder.useMutation(); - const openInAppMutation = electronTrpc.external.openInApp.useMutation(); - const { data: lastUsedApp = "cursor" } = - electronTrpc.settings.getLastUsedApp.useQuery(); - const isRoot = folderPath === ""; + const absolutePath = worktreePath ? isRoot ? worktreePath : `${worktreePath}/${folderPath}` : null; - const handleCopyPath = async () => { - if (absolutePath) { - await navigator.clipboard.writeText(absolutePath); - } - }; - - const handleCopyRelativePath = async () => { - if (folderPath) { - await navigator.clipboard.writeText(folderPath); - } - }; - - const handleRevealInFinder = () => { - if (absolutePath) { - openInFinderMutation.mutate(absolutePath); - } - }; - - const handleOpenInApp = () => { - if (absolutePath) { - openInAppMutation.mutate({ path: absolutePath, app: lastUsedApp }); - } - }; + const { + copyPath, + copyRelativePath, + revealInFinder, + openInEditor, + hasRelativePath, + } = usePathActions({ + absolutePath, + relativePath: folderPath || undefined, + }); const hasContextMenu = worktreePath && folderPath !== undefined; @@ -173,29 +156,27 @@ export function FolderRow({ const contextMenuContent = ( - + Copy Path - {!isRoot && ( - + {hasRelativePath && ( + Copy Relative Path )} - + Reveal in Finder - + Open in Editor - {(onStageAll || onUnstageAll || onDiscardAll) && ( - - )} + {(onStageAll || onUnstageAll || onDiscardAll) && } {onStageAll && ( diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/hooks/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/hooks/index.ts new file mode 100644 index 00000000000..a761619119a --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/hooks/index.ts @@ -0,0 +1 @@ +export { usePathActions } from "./usePathActions"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/hooks/usePathActions.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/hooks/usePathActions.ts new file mode 100644 index 00000000000..b2f3eb09a05 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/hooks/usePathActions.ts @@ -0,0 +1,64 @@ +import { useCallback } from "react"; +import { electronTrpc } from "renderer/lib/electron-trpc"; + +interface UsePathActionsProps { + absolutePath: string | null; + relativePath?: string; + /** For files: pass cwd to use openFileInEditor. For folders: omit to use openInApp */ + cwd?: string; +} + +export function usePathActions({ + absolutePath, + relativePath, + cwd, +}: UsePathActionsProps) { + const openInFinderMutation = electronTrpc.external.openInFinder.useMutation(); + const openInAppMutation = electronTrpc.external.openInApp.useMutation(); + const openFileInEditorMutation = + electronTrpc.external.openFileInEditor.useMutation(); + const { data: lastUsedApp = "cursor" } = + electronTrpc.settings.getLastUsedApp.useQuery(); + + const copyPath = useCallback(async () => { + if (absolutePath) { + await navigator.clipboard.writeText(absolutePath); + } + }, [absolutePath]); + + const copyRelativePath = useCallback(async () => { + if (relativePath) { + await navigator.clipboard.writeText(relativePath); + } + }, [relativePath]); + + const revealInFinder = useCallback(() => { + if (absolutePath) { + openInFinderMutation.mutate(absolutePath); + } + }, [absolutePath, openInFinderMutation]); + + const openInEditor = useCallback(() => { + if (!absolutePath) return; + + if (cwd) { + openFileInEditorMutation.mutate({ path: absolutePath, cwd }); + } else { + openInAppMutation.mutate({ path: absolutePath, app: lastUsedApp }); + } + }, [ + absolutePath, + cwd, + lastUsedApp, + openInAppMutation, + openFileInEditorMutation, + ]); + + return { + copyPath, + copyRelativePath, + revealInFinder, + openInEditor, + hasRelativePath: Boolean(relativePath), + }; +} From 0d3e7205bd917d76f6415c013c3dcf87f71ce2a1 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 28 Jan 2026 09:58:24 -0800 Subject: [PATCH 4/5] Make props required --- .../components/FolderRow/FolderRow.tsx | 42 ++++++------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FolderRow/FolderRow.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FolderRow/FolderRow.tsx index 6ee483e698e..ede28c180d5 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FolderRow/FolderRow.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FolderRow/FolderRow.tsx @@ -31,8 +31,8 @@ interface FolderRowProps { level?: number; fileCount?: number; variant?: "tree" | "grouped"; - folderPath?: string; - worktreePath?: string; + folderPath: string; + worktreePath: string; onStageAll?: () => void; onUnstageAll?: () => void; onDiscardAll?: () => void; @@ -115,25 +115,13 @@ export function FolderRow({ }: FolderRowProps) { const isGrouped = variant === "grouped"; const isRoot = folderPath === ""; + const absolutePath = isRoot ? worktreePath : `${worktreePath}/${folderPath}`; - const absolutePath = worktreePath - ? isRoot - ? worktreePath - : `${worktreePath}/${folderPath}` - : null; - - const { - copyPath, - copyRelativePath, - revealInFinder, - openInEditor, - hasRelativePath, - } = usePathActions({ - absolutePath, - relativePath: folderPath || undefined, - }); - - const hasContextMenu = worktreePath && folderPath !== undefined; + const { copyPath, copyRelativePath, revealInFinder, openInEditor } = + usePathActions({ + absolutePath, + relativePath: folderPath || undefined, + }); const triggerContent = ( Copy Path - {hasRelativePath && ( + {!isRoot && ( Copy Relative Path @@ -211,14 +199,10 @@ export function FolderRow({ onOpenChange={onToggle} className={cn("min-w-0", isGrouped && "overflow-hidden")} > - {hasContextMenu ? ( - - {triggerContent} - {contextMenuContent} - - ) : ( - triggerContent - )} + + {triggerContent} + {contextMenuContent} + Date: Wed, 28 Jan 2026 09:59:37 -0800 Subject: [PATCH 5/5] refactor(desktop): make worktreePath required in file list components Since the parent ChangesView guards against missing worktreePath early in render, the prop is always available when these components are mounted. Tightening the types removes unnecessary optionality. --- .../Sidebar/ChangesView/components/CommitItem/CommitItem.tsx | 2 +- .../Sidebar/ChangesView/components/FileList/FileList.tsx | 2 +- .../ChangesView/components/FileList/FileListGrouped.tsx | 4 ++-- .../Sidebar/ChangesView/components/FileList/FileListTree.tsx | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/CommitItem/CommitItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/CommitItem/CommitItem.tsx index f67656cf1cf..b178784ec9c 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/CommitItem/CommitItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/CommitItem/CommitItem.tsx @@ -12,7 +12,7 @@ interface CommitItemProps { selectedCommitHash: string | null; onFileSelect: (file: ChangedFile, commitHash: string) => void; viewMode: ChangesViewMode; - worktreePath?: string; + worktreePath: string; isExpandedView?: boolean; } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileList/FileList.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileList/FileList.tsx index 481a13d07d0..4ae5c679c07 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileList/FileList.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileList/FileList.tsx @@ -13,7 +13,7 @@ interface FileListProps { onStage?: (file: ChangedFile) => void; onUnstage?: (file: ChangedFile) => void; isActioning?: boolean; - worktreePath?: string; + worktreePath: string; onDiscard?: (file: ChangedFile) => void; category?: ChangeCategory; commitHash?: string; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileList/FileListGrouped.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileList/FileListGrouped.tsx index 22c7e827ecc..3775839798e 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileList/FileListGrouped.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileList/FileListGrouped.tsx @@ -12,7 +12,7 @@ interface FileListGroupedProps { onStage?: (file: ChangedFile) => void; onUnstage?: (file: ChangedFile) => void; isActioning?: boolean; - worktreePath?: string; + worktreePath: string; onDiscard?: (file: ChangedFile) => void; category?: ChangeCategory; commitHash?: string; @@ -66,7 +66,7 @@ interface FolderGroupItemProps { onStage?: (file: ChangedFile) => void; onUnstage?: (file: ChangedFile) => void; isActioning?: boolean; - worktreePath?: string; + worktreePath: string; onDiscard?: (file: ChangedFile) => void; category?: ChangeCategory; commitHash?: string; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileList/FileListTree.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileList/FileListTree.tsx index ec145417a52..6ffb6955884 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileList/FileListTree.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/FileList/FileListTree.tsx @@ -37,7 +37,7 @@ interface FileListTreeProps { onStage?: (file: ChangedFile) => void; onUnstage?: (file: ChangedFile) => void; isActioning?: boolean; - worktreePath?: string; + worktreePath: string; onDiscard?: (file: ChangedFile) => void; category?: ChangeCategory; commitHash?: string; @@ -106,7 +106,7 @@ interface TreeNodeComponentProps { onStage?: (file: ChangedFile) => void; onUnstage?: (file: ChangedFile) => void; isActioning?: boolean; - worktreePath?: string; + worktreePath: string; onDiscard?: (file: ChangedFile) => void; category?: ChangeCategory; commitHash?: string;