diff --git a/src/backend/list-routes.ts b/src/backend/list-routes.ts index 670c959..bca831c 100644 --- a/src/backend/list-routes.ts +++ b/src/backend/list-routes.ts @@ -346,8 +346,7 @@ export function registerFileListingRoutes( const tryWithSudo = () => { try { const escapedPath = sshPath.replace(/'/g, "'\"'\"'"); - const escapedPassword = sshConn.sudoPassword!.replace(/'/g, "'\"'\"'"); - const sudoCommand = `echo '${escapedPassword}' | sudo -S /bin/ls -la --color=never '${escapedPath}' 2>&1`; + const sudoCommand = `sudo -S -p '' /bin/ls -la --color=never '${escapedPath}' 2>&1`; execChannel(sshConn, sudoCommand, (err, stream) => { if (err) { @@ -359,6 +358,9 @@ export function registerFileListingRoutes( let data = ""; let errorData = ""; + // stdin, not argv: keeps the password out of the process list + stream.write(`${sshConn.sudoPassword}\n`); + stream.on("data", (chunk: Buffer) => { data += chunk.toString(); }); diff --git a/src/frontend/FileManager.tsx b/src/frontend/FileManager.tsx index 3ed87df..498d6fe 100644 --- a/src/frontend/FileManager.tsx +++ b/src/frontend/FileManager.tsx @@ -14,6 +14,7 @@ import React, { import { asHttpError } from "./lib/http-error"; import { cn, PanelShell } from "@termix-ssh/plugin-sdk/ui"; import { FileManagerGrid } from "./FileManagerGrid.tsx"; +import { filterAndSortFiles } from "./file-list.ts"; import { FileManagerSidebar, type SidebarItem } from "./FileManagerSidebar.tsx"; import { FileManagerContextMenu } from "./FileManagerContextMenu.tsx"; import { useFileSelection } from "./hooks/useFileSelection.ts"; @@ -3591,32 +3592,7 @@ function FileManagerContent({ }, [sortBy, sortOrder]); const filteredFiles = useMemo( - () => - files - .filter((file) => - file.name.toLowerCase().includes(searchQuery.toLowerCase()), - ) - .sort((a, b) => { - if (a.type === "directory" && b.type !== "directory") return -1; - if (a.type !== "directory" && b.type === "directory") return 1; - - let result = 0; - switch (sortBy) { - case "name": - result = a.name.localeCompare(b.name, undefined, { - numeric: true, - sensitivity: "base", - }); - break; - case "modified": - result = (a.modifiedTimestamp || 0) - (b.modifiedTimestamp || 0); - break; - case "size": - result = (a.size || 0) - (b.size || 0); - break; - } - return sortOrder === "desc" ? -result : result; - }), + () => filterAndSortFiles(files, searchQuery, sortBy, sortOrder), [files, searchQuery, sortBy, sortOrder], ); diff --git a/src/frontend/FileManagerGrid.tsx b/src/frontend/FileManagerGrid.tsx index 4e7fd45..bf0b197 100644 --- a/src/frontend/FileManagerGrid.tsx +++ b/src/frontend/FileManagerGrid.tsx @@ -304,6 +304,14 @@ export function FileManagerGrid({ return () => ro.disconnect(); }, [viewMode, CONTENT_PAD, GRID_GAP, GRID_CELL]); + const selectedPaths = useMemo( + () => new Set(selectedFiles.map((f) => f.path)), + [selectedFiles], + ); + const dragPaths = useMemo( + () => new Set(dragState.files.map((f) => f.path)), + [dragState.files], + ); const gridRowCount = useMemo( () => (viewMode === "grid" ? Math.ceil(files.length / gridCols) : 0), [viewMode, files.length, gridCols], @@ -348,6 +356,27 @@ export function FileManagerGrid({ // entries would otherwise keep this folder's measured row heights. }, [viewMode, density, files, editingFile?.path, createIntent, gridCols]); + // A hidden tab (display:none) collapses the scroller and the browser resets + // its scrollTop without a scroll event, leaving the virtualizers with a stale + // offset and rect. Re-sync them when the scroller becomes visible again. + useEffect(() => { + const el = gridRef.current; + if (!el) return; + let wasHidden = el.clientHeight === 0; + const ro = new ResizeObserver(() => { + const hidden = el.clientHeight === 0; + if (wasHidden && !hidden) { + for (const v of [listVirtualizer, gridVirtualizer]) { + v.scrollOffset = el.scrollTop; + v.measure(); + } + } + wasHidden = hidden; + }); + ro.observe(el); + return () => ro.disconnect(); + }, [listVirtualizer, gridVirtualizer]); + useEffect(() => { const handleGlobalMouseMove = (e: MouseEvent) => { if (dragState.type === "internal" && dragState.files.length > 0) { @@ -900,7 +929,7 @@ export function FileManagerGrid({ onSelectionChange(rangeFiles); } } else if (multiSelect) { - const isSelected = selectedFiles.some((f) => f.path === file.path); + const isSelected = selectedPaths.has(file.path); if (isSelected) { onSelectionChange(selectedFiles.filter((f) => f.path !== file.path)); } else { @@ -1218,9 +1247,7 @@ export function FileManagerGrid({ }} > {rowFiles.map((file) => { - const isSelected = selectedFiles.some( - (f) => f.path === file.path, - ); + const isSelected = selectedPaths.has(file.path); return (